0%

1. RISC-V: Add initial skeletal KVM support

This patch adds initial skeletal KVM RISC-V support which has:
1. A simple implementation of arch specific VM functions
   except kvm_vm_ioctl_get_dirty_log() which will implemeted
   in-future as part of stage2 page loging.
2. Stubs of required arch specific VCPU functions except
   kvm_arch_vcpu_ioctl_run() which is semi-complete and
   extended by subsequent patches.
3. Stubs for required arch specific stage2 MMU functions.

这个补丁增加了初始的KVM RISC-V 框架
它具有。

  1. 除了kvm_vm_ioctl_get_dirty_log()之外,一个简单的arch特定虚拟机函数的实现,它将作为第二阶段页面记录的一部分在未来实施。
  2. 除了kvm_arch_vcpu_ioctl_run()是半完全的并由后续补丁扩展外,所需的特定arch VCPU函数的stub。
  3. 所需的arch 特定第二阶段MMU功能的 stub 。

2. RISC-V: KVM: Implement VCPU create, init and destroy functions

This patch implements VCPU create, init and destroy functions
required by generic KVM module. We don't have much dynamic
resources in struct kvm_vcpu_arch so these functions are quite
simple for KVM RISC-V.

这个补丁实现了通用KVM模块所需的VCPU创建、启动和销毁功能。我们在结构kvm_vcpu_arch中没有很多动态资源,所以这些函数对于KVM RISC-V来说非常简单。

3. RISC-V: KVM: Implement VCPU interrupts and requests handling

This patch implements VCPU interrupts and requests which are both
asynchronous events.

The VCPU interrupts can be set/unset using KVM_INTERRUPT ioctl from
user-space. In future, the in-kernel IRQCHIP emulation will use
kvm_riscv_vcpu_set_interrupt() and kvm_riscv_vcpu_unset_interrupt()
functions to set/unset VCPU interrupts.

Important VCPU requests implemented by this patch are:
KVM_REQ_SLEEP       - set whenever VCPU itself goes to sleep state
KVM_REQ_VCPU_RESET  - set whenever VCPU reset is requested

The WFI trap-n-emulate (added later) will use KVM_REQ_SLEEP request
and kvm_riscv_vcpu_has_interrupt() function.

The KVM_REQ_VCPU_RESET request will be used by SBI emulation (added
later) to power-up a VCPU in power-off state. The user-space can use
the GET_MPSTATE/SET_MPSTATE ioctls to get/set power state of a VCPU.

这个补丁实现了VCPU中断和请求,它们都是异步事件。
VCPU中断可以使用用户空间的KVM_INTERRUPT ioctl进行设置/取消。
在未来,内核IRQCHIP仿真将使用kvm_riscv_vcpu_set_interrupt()和kvm_riscv_vcpu_unset_interrupt()函数来设置/取消VCPU中断。
这个补丁实现的重要VCPU请求是。
KVM_REQ_SLEEP -每当VCPU本身进入睡眠状态时设置
KVM_REQ_VCPU_RESET -每当VCPU复位时设置
WFI trap-n-emulate(稍后添加)将使用KVM_REQ_SLEEP请求和kvm_riscv_vcpu_has_interrupt()函数。
KVM_REQ_VCPU_RESET请求将被SBI仿真使用(稍后添加),以使VCPU处于断电状态。
用户空间可以使用GET_MPSTATE/SET_MPSTATE ioctls来获取/设置VCPU的电源状态。

4. RISC-V: KVM: Implement KVM_GET_ONE_REG/KVM_SET_ONE_REG ioctls

For KVM RISC-V, we use KVM_GET_ONE_REG/KVM_SET_ONE_REG ioctls to access
VCPU config and registers from user-space.

We have three types of VCPU registers:
1. CONFIG - these are VCPU config and capabilities
2. CORE   - these are VCPU general purpose registers
3. CSR    - these are VCPU control and status registers

The CONFIG register available to user-space is ISA. The ISA register is
a read and write register where user-space can only write the desired
VCPU ISA capabilities before running the VCPU.

The CORE registers available to user-space are PC, RA, SP, GP, TP, A0-A7,
T0-T6, S0-S11 and MODE. Most of these are RISC-V general registers except
PC and MODE. The PC register represents program counter whereas the MODE
register represent VCPU privilege mode (i.e. S/U-mode).

The CSRs available to user-space are SSTATUS, SIE, STVEC, SSCRATCH, SEPC,
SCAUSE, STVAL, SIP, and SATP. All of these are read/write registers.

In future, more VCPU register types will be added (such as FP) for the
KVM_GET_ONE_REG/KVM_SET_ONE_REG ioctls.

对于KVM RISC-V,我们使用KVM_GET_ONE_REG/KVM_SET_ONE_REG ioctls来访问VCPU配置和用户空间的寄存器。
我们有三种类型的VCPU寄存器。

  1. CONFIG - 这些是VCPU配置和能力
  2. CORE - 这些是VCPU通用寄存器
  3. CSR–这些是VCPU控制和状态寄存器
    用户空间可用的CONFIG寄存器是ISA。ISA寄存器是一个读写寄存器,用户空间在运行VCPU之前只能写入所需的VCPU ISA功能。用户空间可用的CORE寄存器是PC、RA、SP、GP、TP、A0-A7、T0-T6、S0-S11和MODE。其中大部分是RISC-V通用寄存器,除了PC和MODE。PC寄存器代表程序计数器,而MODE寄存器代表VCPU的特权模式(即S/U模式)。
    用户空间可用的CSR有SSTATUS、SIE、STVEC、SSCRATCH、SEPC、SCAUSE、STVAL、SIP和SATP。
    所有这些都是读/写寄存器。

在未来,更多的VCPU寄存器类型将被添加到KVM_GET_ONE_REG/KVM_SET_ONE_REG ioctls中(如FP)。

5. RISC-V: KVM: Implement VCPU world-switch

This patch implements the VCPU world-switch for KVM RISC-V.

The KVM RISC-V world-switch (i.e. __kvm_riscv_switch_to()) mostly
switches general purpose registers, SSTATUS, STVEC, SSCRATCH and
HSTATUS CSRs. Other CSRs are switched via vcpu_load() and vcpu_put()
interface in kvm_arch_vcpu_load() and kvm_arch_vcpu_put() functions
respectively.

这个补丁实现了KVM RISC-V的VCPU world 切换。KVM RISC-V世界切换(即__kvm_riscv_switch_to())大多切换通用寄存器、SSTATUS、STVEC、SSCRATCH和HSTATUS CSR。其他CSR分别通过kvm_arch_vcpu_load()和kvm_arch_vcpu_put()函数中的vcpu_load()和vcpu_put()接口进行切换。

6. RISC-V: KVM: Handle MMIO exits for VCPU

We will get stage2 page faults whenever Guest/VM access SW emulated
MMIO device or unmapped Guest RAM.

This patch implements MMIO read/write emulation by extracting MMIO
details from the trapped load/store instruction and forwarding the
MMIO read/write to user-space. The actual MMIO emulation will happen
in user-space and KVM kernel module will only take care of register
updates before resuming the trapped VCPU.

The handling for stage2 page faults for unmapped Guest RAM will be
implemeted by a separate patch later.

每当Guest/VM访问SW模拟的MMIO设备或未映射的Guest RAM时,我们将得到阶段2的page fault。这个补丁通过从被困的加载/存储指令中提取MMIO 指令细节并将MMIO读/写转发到用户空间来实现MMIO读/写仿真。实际的MMIO仿真将发生在用户空间,KVM内核模块将只负责在恢复被困的VCPU之前进行寄存器更新。对未映射的Guest RAM的第2阶段page fault的处理将由一个单独的补丁来实现。

7. RISC-V: KVM: Handle WFI exits for VCPU

We get illegal instruction trap whenever Guest/VM executes WFI
instruction.

This patch handles WFI trap by blocking the trapped VCPU using
kvm_vcpu_block() API. The blocked VCPU will be automatically
resumed whenever a VCPU interrupt is injected from user-space
or from in-kernel IRQCHIP emulation.

每当Guest/VM执行WFI指令时,我们会得到非法指令陷阱。这个补丁通过使用kvm_vcpu_block()API阻塞VCPU来处理WFI陷阱。每当从用户空间或内核IRQCHIP仿真中注入VCPU中断时,被阻塞的VCPU将被自动恢复。

8. RISC-V: KVM: Implement VMID allocator

We implement a simple VMID allocator for Guests/VMs which:
1. Detects number of VMID bits at boot-time
2. Uses atomic number to track VMID version and increments
   VMID version whenever we run-out of VMIDs
3. Flushes Guest TLBs on all host CPUs whenever we run-out
   of VMIDs
4. Force updates HW Stage2 VMID for each Guest VCPU whenever
   VMID changes using VCPU request KVM_REQ_UPDATE_HGATP

我们为guest/虚拟机实现了一个简单的VMID分配器

  1. 在启动时检测VMID位的数量。
  2. 使用原子序数来跟踪VMID版本,并在我们用完VMID时增加VMID版本
  3. 每当我们用完VMID时,就会在所有host CPU上刷新Guest TLB
  4. 每当VMID发生变化时,使用VCPU请求KVM_REQ_UPDATE_HGATP为每个guest VCPU强制更新HW Stage2 VMID

9. RISC-V: KVM: Implement stage2 page table programming

This patch implements all required functions for programming
the stage2 page table for each Guest/VM.

At high-level, the flow of stage2 related functions is similar
from KVM ARM/ARM64 implementation but the stage2 page table
format is quite different for KVM RISC-V.

这个补丁实现了为每个guest/虚拟机的stage2页表编程的所有必要功能。在高层次上,阶段2相关函数的流程与KVM ARM/ARM64实现相似,但阶段2页表格式与KVM RISC-V有很大不同。

提供直接编程 stage-2 页表的接口
kvm_riscv_gstage_alloc_pgd/kvm_riscv_gstage_free_pgd
gstage_get_leaf_entry
gstage_pte_page_vaddr 等

arch/riscv/kvm/mmu.c

10. RISC-V: KVM: Implement MMU notifiers ???

This patch implements MMU notifiers for KVM RISC-V so that Guest
physical address space is in-sync with Host physical address space.

This will allow swapping, page migration, etc to work transparently
with KVM RISC-V.

这个补丁为KVM RISC-V实现了MMU通知器,以便Guest物理地址空间与Host物理地址空间同步。这将允许交换、页面迁移等与KVM RISC-V透明地工作。

当KVM_CAP_SYNC_MMU功能可用时,备份内存区域的变化会自动反映到guest中。例如,一个影响该区域的mmap()将被立即变成可见。另一个例子是madvise(MADV_DROP)。

11. RISC-V: KVM: Add timer functionality

The RISC-V hypervisor specification doesn't have any virtual timer
feature.

Due to this, the guest VCPU timer will be programmed via SBI calls.
The host will use a separate hrtimer event for each guest VCPU to
provide timer functionality. We inject a virtual timer interrupt to
the guest VCPU whenever the guest VCPU hrtimer event expires.

This patch adds guest VCPU timer implementation along with ONE_REG
interface to access VCPU timer state from user space.

RISC-V hypervisor 规范没有任何虚拟定时器功能。由于这个原因,guest VCPU定时器将通过SBI调用进行编程。host os将为每个客户VCPU使用一个单独的hrtimer事件来提供定时器功能。每当客体VCPU的hrtimer事件过期时,我们就向客体VCPU注入一个虚拟定时器中断。这个补丁增加了客户VCPU定时器的实现以及ONE_REG接口,以便从用户空间访问VCPU定时器的状态。

12. RISC-V: KVM: FP lazy save/restore

This patch adds floating point (F and D extension) context save/restore
for guest VCPUs. The FP context is saved and restored lazily only when
kernel enter/exits the in-kernel run loop and not during the KVM world
switch. This way FP save/restore has minimal impact on KVM performance.

这个补丁为客户VCPU增加了浮点(F和D扩展)上下文保存/恢复。只有在内核进入/退出 run-loop 时,才会延迟的保存和恢复FP上下文,而不是在KVM世界切换时。这样,FP保存/恢复对KVM性能的影响就很小。

13. RISC-V: KVM: Implement ONE REG interface for FP registers

Add a KVM_GET_ONE_REG/KVM_SET_ONE_REG ioctl interface for floating
point registers such as F0-F31 and FCSR. This support is added for
both 'F' and 'D' extensions.

为F0-F31和FCSR等浮点寄存器添加一个KVM_GET_ONE_REG/KVM_SET_ONE_REG ioctl接口。这个支持是为F和D的扩展添加的。

14. RISC-V: KVM: Add SBI v0.1 support

The KVM host kernel is running in HS-mode needs so we need to handle
the SBI calls coming from guest kernel running in VS-mode.

This patch adds SBI v0.1 support in KVM RISC-V. Almost all SBI v0.1
calls are implemented in KVM kernel module except GETCHAR and PUTCHART
calls which are forwarded to user space because these calls cannot be
implemented in kernel space. In future, when we implement SBI v0.2 for
Guest, we will forward SBI v0.2 experimental and vendor extension calls
to user space.

KVM host os 以HS模式运行,所以我们需要处理来自VS模式下运行的 guest os的SBI调用。这个补丁在KVM RISC-V中增加了SBI v0.1支持。除了GETCHAR和PUTCHART调用被转发到用户空间,几乎所有SBI v0.1调用都在KVM内核模块中实现,因为这些调用不能在内核空间中实现。在未来,当我们为Guest实现SBI v0.2时,我们将把SBI v0.2的实验和vendor 扩展调用转发给用户空间。

16. RISC-V: KVM: Fix GPA passed to __kvm_riscv_hfence_gvma_xyz() functions

The parameter passed to HFENCE.GVMA instruction in rs1 register
is guest physical address right shifted by 2 (i.e. divided by 4).

Unfortunately, we overlooked the semantics of rs1 registers for
HFENCE.GVMA instruction and never right shifted guest physical
address by 2. This issue did not manifest for hypervisors till
now because:
  1) Currently, only __kvm_riscv_hfence_gvma_all() and SBI
     HFENCE calls are used to invalidate TLB.
  2) All H-extension implementations (such as QEMU, Spike,
     Rocket Core FPGA, etc) that we tried till now were
     conservatively flushing everything upon any HFENCE.GVMA
     instruction.

This patch fixes GPA passed to __kvm_riscv_hfence_gvma_vmid_gpa()
and __kvm_riscv_hfence_gvma_gpa() functions.

rs1寄存器中传递给HFENCE.GVMA指令的参数是GPA右移2(即除以4)。不幸的是,我们忽略了HFENCE.GVMA指令的rs1寄存器的语义,也没有将客户的物理地址右移2。这个问题直到现在还没有在管理程序中表现出来,
因为:
1)目前,只有__kvm_riscv_hfence_gvma_all()和SBI HFENCE调用被用来使TLB失效。
2)到目前为止,我们尝试的所有H扩展实现(如QEMU、Spike、Rocket Core FPGA等)都是保守地在任何HFENCE.GVMA指令上刷新一切。
这个补丁修复了传递给__kvm_riscv_hfence_gvma_vmid_gpa()和__kvm_riscv_hfence_gvma_gpa()函数的GPA。

bug, 不关注

17. KVM: RISC-V: Unmap stage2 mapping when deleting/moving a memslot

Unmap stage2 page tables when a memslot is being deleted or moved.  It's
the architectures' responsibility to ensure existing mappings are removed
when kvm_arch_flush_shadow_memslot() returns.

当一个memslot被删除或移动时,unmap stage2页表。当kvm_arch_flush_shadow_memslot()返回时,架构有责任确保现有的映射被移除。

18. KVM: Let/force architectures to deal with arch specific memslot data

Pass the "old" slot to kvm_arch_prepare_memory_region() and force arch
code to handle propagating arch specific data from "new" to "old" when
necessary.  This is a baby step towards dynamically allocating "new" from
the get go, and is a (very) minor performance boost on x86 due to not
unnecessarily copying arch data.

For PPC HV, copy the rmap in the !CREATE and !DELETE paths, i.e. for MOVE
and FLAGS_ONLY.  This is functionally a nop as the previous behavior
would overwrite the pointer for CREATE, and eventually discard/ignore it
for DELETE.

For x86, copy the arch data only for FLAGS_ONLY changes.  Unlike PPC HV,
x86 needs to reallocate arch data in the MOVE case as the size of x86's
allocations depend on the alignment of the memslot's gfn.

Opportunistically tweak kvm_arch_prepare_memory_region()'s param order to
match the "commit" prototype.

将 “old slot “传递给kvm_arch_prepare_memory_region(),必要时强制arch-specific 代码处理从 “new “传播到 “old “的arch 特定数据。这是朝着动态分配 “从头开始的新 “迈出的一小步,也是对x86的一个(非常)小的性能提升,因为没有不必要地复制arch 数据。
对于PPC HV,在!CREATE和!DELETE路径中复制rmap,即对于MOVE和FLAGS_ONLY。这在功能上是一个问题,因为之前的行为会覆盖CREATE的指针,并最终丢弃/忽略它用于DELETE。对于x86,只复制FLAGS_ONLY变化的档案数据。与PPC HV不同,在MOVE情况下,x86需要重新分配arch 数据,因为x86 分配的大小取决于memslot gfn的排列。
机会性地调整kvm_arch_prepare_memory_region() param顺序以匹配 “commit “原型。

内存memslot 动态分配相关优化

19. KVM: RISC-V: Use “new” memslot instead of userspace memory region ???

Get the slot ID, hva, etc... from the "new" memslot instead of the
userspace memory region when preparing/committing a memory region.  This
will allow a future commit to drop @mem from the prepare/commit hooks
once all architectures convert to using "new".

Opportunistically wait to get the various "new" values until after
filtering out the DELETE case in anticipation of a future commit passing
NULL for @new when deleting a memslot.

在准备/提交内存区域时,从 “new- memslot “而不是用户空间内存区域获取slot ID、hva等。这将允许未来的提交在所有架构转换为使用 “新 “时,从准备/提交的钩子中删除@mem。机会性地等待得到各种 “new 值 , 直到过滤掉 DELETE 的情况后 , 以期待未来的提交在删除一个 memslot 时通过 @new 的 NULL

20. KVM: RISC-V: Use common KVM implementation of MMU memory caches

Use common KVM's implementation of the MMU memory caches, which for all
intents and purposes is semantically identical to RISC-V's version, the
only difference being that the common implementation will fall back to an
atomic allocation if there's a KVM bug that triggers a cache underflow.

RISC-V appears to have based its MMU code on arm64 before the conversion
to the common caches in commit c1a33aebe91d ("KVM: arm64: Use common KVM
implementation of MMU memory caches"), despite having also copy-pasted
the definition of KVM_ARCH_NR_OBJS_PER_MEMORY_CACHE in kvm_types.h.

Opportunistically drop the superfluous wrapper
kvm_riscv_stage2_flush_cache(), whose name is very, very confusing as
"cache flush" in the context of MMU code almost always refers to flushing
hardware caches, not freeing unused software objects.

No functional change intended.

使用MMU内存缓存的普通KVM实现,就所有的意图和目的而言,它与RISC-V-s版本在语义上是相同的,唯一的区别是,如果有一个KVM错误触发了缓存下溢,普通实现将退回到原子分配。RISC-V似乎在转换到提交c1a33aebe91d中的普通缓存之前将其MMU代码基于arm64(KVM:arm64:使用MMU内存缓存的普通KVM实现),尽管在kvm_types.h中也复制了KVM_ARCH_NR_OBJS_PER_MEMORY_CACHE的定义。机会性地放弃多余的包装器kvm_riscv_stage2_flush_cache(),其名称非常非常令人困惑,因为在MMU代码的上下文中,缓存刷新 “几乎总是指刷新硬件缓存,而不是释放未使用的软件对象。没有功能变化的意图。

21. RISC-V: KVM: Add SBI v0.2 base extension

SBI v0.2 base extension defined to allow backward compatibility and
probing of future extensions. This is also the only mandatory SBI
extension that must be implemented by SBI implementors.

SBI v0.2基础扩展的定义是允许向后兼容和探测未来的扩展。这也是唯一的强制性SBI扩展,必须由SBI实现者来实现。

22. RISC-V: KVM: Add SBI HSM extension in KVM

SBI HSM extension allows OS to start/stop harts any time. It also allows
ordered booting of harts instead of random booting.

Implement SBI HSM exntesion and designate the vcpu 0 as the boot vcpu id.
All other non-zero non-booting vcpus should be brought up by the OS
implementing HSM extension. If the guest OS doesn't implement HSM
extension, only single vcpu will be available to OS.

SBI HSM扩展允许操作系统在任何时候开始/停止 harts。它还允许有序启动 hart,而不是随机启动。实施SBI HSM exntesion,并将vcpu 0指定为boot vcpu id。所有其他非零的非启动vcpus都应该由实现HSM扩展的操作系统提出。如果guest os 没有实现HSM扩展,那么只有单个vcpu可以被操作系统使用。

23. RISC-V: KVM: Forward SBI experimental and vendor extensions

The SBI experimental extension space is for temporary (or experimental)
stuff whereas SBI vendor extension space is for hardware vendor specific
stuff. Both these SBI extension spaces won't be standardized by the SBI
specification so let's blindly forward such SBI calls to the userspace.

SBI实验性扩展空间用于临时(或实验性)特性,而SBI vendor 扩展空间用于硬件vendor 特性。这两个SBI扩展空间都不会被SBI规范化,我们可以盲目地将这种SBI调用转发给 kvm的 用户空间。

24. KVM: RISC-V: Avoid spurious virtual interrupts after clearing hideleg CSR

避免在清除隐藏的CSR后出现虚假的虚拟中断
When the last VM is terminated, the host kernel will invoke function
hardware_disable_nolock() on each CPU to disable the related virtualization
functions. Here, RISC-V currently only clears hideleg CSR and hedeleg CSR.
This behavior will cause the host kernel to receive spurious interrupts if
hvip CSR has pending interrupts and the corresponding enable bits in vsie
CSR are asserted. To avoid it, hvip CSR and vsie CSR must be cleared
before clearing hideleg CSR.

当最后一个虚拟机被终止时,主机内核将在每个CPU上调用函数hardhard_disable_nolock()来禁用相关的虚拟化函数。在这里,RISC-V目前只清除了hideleg CSR和hedeleg CSR。如果hvip CSR有待定的中断,并且vsie CSR中相应的启用位被断言,这种行为将导致主机内核收到虚假的中断。为了避免这种情况,hvip CSR和vsie CSR必须在清除隐藏的CSR之前被清除。

25. kvm/riscv: rework guest entry logic

In kvm_arch_vcpu_ioctl_run() we enter an RCU extended quiescent state
(EQS) by calling guest_enter_irqoff(), and unmask IRQs prior to exiting
the EQS by calling guest_exit(). As the IRQ entry code will not wake RCU
in this case, we may run the core IRQ code and IRQ handler without RCU
watching, leading to various potential problems.

Additionally, we do not inform lockdep or tracing that interrupts will
be enabled during guest execution, which caan lead to misleading traces
and warnings that interrupts have been enabled for overly-long periods.

This patch fixes these issues by using the new timing and context
entry/exit helpers to ensure that interrupts are handled during guest
vtime but with RCU watching, with a sequence:

        guest_timing_enter_irqoff();

        guest_state_enter_irqoff();
        < run the vcpu >
        guest_state_exit_irqoff();

        < take any pending IRQs >

        guest_timing_exit_irqoff();

Since instrumentation may make use of RCU, we must also ensure that no
instrumented code is run during the EQS. I've split out the critical
section into a new kvm_riscv_enter_exit_vcpu() helper which is marked
noinstr.

RCU锁及开关中断相关优化

26. RISC-V: KVM: Add common kvm_riscv_vcpu_sbi_system_reset() function

We rename kvm_sbi_system_shutdown() to kvm_riscv_vcpu_sbi_system_reset()
and move it to vcpu_sbi.c so that it can be shared by SBI v0.1 shutdown
and SBI v0.3 SRST extension.

我们将kvm_sbi_system_shutdown()重命名为kvm_riscv_vcpu_sbi_system_reset(),并将其移至vcpu_sbi.c,以便它可以被SBI v0.1关闭和SBI v0.3 SRST扩展共享。

The SBI v0.3 specification defines SRST (System Reset) extension which
provides a standard poweroff and reboot interface. This patch implements
SRST extension for the KVM Guest.

SBI v0.3规范定义了SRST(系统重置)扩展,它提供了一个标准的断电和重启接口。这个补丁实现了KVM Guest的SRST扩展。
guest os 重启

27. RISC-V: KVM: Add common kvm_riscv_vcpu_wfi() function

The wait for interrupt (WFI) instruction emulation can share the VCPU
halt logic with SBI HSM suspend emulation so this patch adds a common
kvm_riscv_vcpu_wfi() function for this purpose.

等待中断(WFI)指令仿真可以与SBI HSM暂停仿真共享VCPU停止逻辑,所以这个补丁为此增加了一个通用的kvm_riscv_vcpu_wfi()函数。

28. RISC-V: KVM: Implement SBI HSM suspend call

The SBI v0.3 specification extends SBI HSM extension by adding SBI HSM
suspend call and related HART states. This patch extends the KVM RISC-V
HSM implementation to provide KVM guest a minimal SBI HSM suspend call
which is equivalent to a WFI instruction.

SBI v0.3规范通过添加SBI HSM暂停调用和相关的HART状态扩展了SBI HSM的扩展。这个补丁扩展了KVM RISC-V HSM的实现,为KVM guest os提供了一个最小的SBI HSM暂停调用,相当于一个WFI指令。

29. RISC-V: KVM: Add Sv57x4 mode support for G-stage

Latest QEMU supports G-stage Sv57x4 mode so this patch extends KVM
RISC-V G-stage handling to detect and use Sv57x4 mode when available.

最新的QEMU支持G阶段的Sv57x4模式,所以这个补丁扩展了KVM RISC-V G阶段的处理,以检测并在可用时使用Sv57x4模式。

30. RISC-V: KVM: Treat SBI HFENCE calls as NOPs

We should treat SBI HFENCE calls as NOPs until nested virtualization
is supported by KVM RISC-V. This will help us test booting a hypervisor
under KVM RISC-V.

我们应该把SBI HFENCE调用当作NOP,直到KVM RISC-V支持嵌套虚拟化。这将有助于我们在KVM RISC-V下测试启动一个管理程序。

31. RISC-V: KVM: Add remote HFENCE functions based on VCPU requests

The generic KVM has support for VCPU requests which can be used
to do arch-specific work in the run-loop. We introduce remote
HFENCE functions which will internally use VCPU requests instead
of host SBI calls.

Advantages of doing remote HFENCEs as VCPU requests are:
1) Multiple VCPUs of a Guest may be running on different Host CPUs
   so it is not always possible to determine the Host CPU mask for
   doing Host SBI call. For example, when VCPU X wants to do HFENCE
   on VCPU Y, it is possible that VCPU Y is blocked or in user-space
   (i.e. vcpu->cpu < 0).
2) To support nested virtualization, we will be having a separate
   shadow G-stage for each VCPU and a common host G-stage for the
   entire Guest/VM. The VCPU requests based remote HFENCEs helps
   us easily synchronize the common host G-stage and shadow G-stage
   of each VCPU without any additional IPI calls.

This is also a preparatory patch for upcoming nested virtualization
support where we will be having a shadow G-stage page table for
each Guest VCPU.

通用的KVM支持VCPU请求,可以用来在run-loop中做arch specific工作。我们引入了远程HFENCE函数,它在内部将使用VCPU请求而不是主机SBI调用。

作为VCPU请求做远程HFENCE的好处是:
1)一个guest os的多个VCPU可能在不同的物理 CPU上运行,所以并不总是能够确定物理cpuCPU掩码来做主机SBI调用。例如,当VCPU X想在VCPU Y上做HFENCE时,有可能VCPU Y被阻塞或在用户空间(即vcpu->cpu < 0)。
2)为了支持嵌套虚拟化,我们将为每个VCPU提供一个单独的影子G阶段,为整个Guest/VM提供一个通用的G-stage 页表。基于VCPU请求的远程HFENCEs帮助我们轻松地同步每个VCPU的物理的G-stage页表和独立的影子G-stage页表,而不需要任何额外的IPI调用。

这也是即将到来的嵌套虚拟化支持的一个准备补丁,我们将为每个guest VCPU提供一个影子G阶段的页表。

32. RISC-V: KVM: Cleanup stale TLB entries when host CPU changes

On RISC-V platforms with hardware VMID support, we share same
VMID for all VCPUs of a particular Guest/VM. This means we might
have stale G-stage TLB entries on the current Host CPU due to
some other VCPU of the same Guest which ran previously on the
current Host CPU.

To cleanup stale TLB entries, we simply flush all G-stage TLB
entries by VMID whenever underlying Host CPU changes for a VCPU.

VMID 相关. 增加在重新执行vcpu时刷新所有G-stage tlb的功能.

在支持硬件VMID的RISC-V平台上,我们为一个特定的guest/VM的所有VCPU共享相同的VMID。这意味着我们在当前的主机CPU上可能有陈旧的G级TLB条目,这是因为之前在当前的主机CPU上运行的同一guest的其他VCPU。为了清理陈旧的TLB条目,我们只需在底层主机CPU为VCPU改变时,通过VMID刷新所有G级TLB条目。

33. RISC-V: KVM: Add extensible system instruction emulation framework

We will be emulating more system instructions in near future with
upcoming AIA, PMU, Nested and other virtualization features.

To accommodate above, we add an extensible system instruction emulation
framework in vcpu_insn.c.

我们将在不久的将来用即将到来的AIA、PMU、Nested和其他虚拟化功能模拟更多的系统指令。为了适应上述情况,我们在vcpu_insn.c中增加了一个可扩展的系统指令仿真框架。

34. RISC-V: KVM: Add extensible CSR emulation framework

We add an extensible CSR emulation framework which is based upon the
existing system instruction emulation. This will be useful to upcoming
AIA, PMU, Nested and other virtualization features.

The CSR emulation framework also has provision to emulate CSR in user
space but this will be used only in very specific cases such as AIA
IMSIC CSR emulation in user space or vendor specific CSR emulation
in user space.

By default, all CSRs not handled by KVM RISC-V will be redirected back
to Guest VCPU as illegal instruction trap.

我们增加了一个可扩展的CSR仿真框架,它是基于现有的系统指令仿真的。这对即将到来的AIA、PMU、Nested和其他虚拟化功能很有用。CSR仿真框架也有在用户空间仿真CSR的规定,但这将只用于非常特殊的情况,如AIA IMSIC CSR仿真在用户空间或vendor厂商特定的CSR仿真在用户空间。默认情况下,所有未被KVM RISC-V处理的CSR将被重定向回Guest VCPU作为非法指令陷阱。

35. RISC-V: KVM: Add G-stage ioremap() and iounmap() functions

The in-kernel AIA IMSIC support requires on-demand mapping / unmapping
of Guest IMSIC address to Host IMSIC guest files. To help achieve this,
we add kvm_riscv_stage2_ioremap() and kvm_riscv_stage2_iounmap() functions.
These new functions for updating G-stage page table mappings will be called
in atomic context so we have special "in_atomic" parameter for this purpose.

中断AIA IMSIC 相关
内核 AIA IMSIC 的支持需要按需将guest os IMSIC 地址映射 / 解除映射到主机 IMSIC guest file。为了帮助实现这一点,我们增加了kvm_riscv_stage2_ioremap()和kvm_riscv_stage2_iounmap()函数。这些用于更新G阶段页表映射的新函数将在原子上下文中被调用,因此我们有特殊的in_atomic 参数来实现这一目的。

36. RISC-V: KVM: Add support for Svpbmt inside Guest/VM

The Guest/VM can use Svpbmt in VS-stage page tables when allowed by the
Hypervisor using the henvcfg.PBMTE bit.

We add Svpbmt support for the KVM Guest/VM which can be enabled/disabled
by the KVM user-space (QEMU/KVMTOOL) using the ISA extension ONE_REG
interface.

当Hypervisor使用henvcfg.PBMTE位允许时,Guest/VM可以在VS阶段的页表中使用Svpbmt。我们为KVM Guest/VM添加了Svpbmt支持,它可以通过KVM用户空间(QEMU/KVMTOOL)使用ISA扩展ONE_REG接口启用/禁用。

37. RISC-V: KVM: Support sstc extension

Sstc extension allows the guest to program the vstimecmp CSR directly
instead of making an SBI call to the hypervisor to program the next
event. The timer interrupt is also directly injected to the guest by
the hardware in this case. To maintain backward compatibility, the
hypervisors also update the vstimecmp in an SBI set_time call if
the hardware supports it. Thus, the older kernels in guest also
take advantage of the sstc extension.

Sstc扩展允许guest os直接对vstimecmp CSR进行编程,而不是对管理程序进行SBI调用来对下一个事件进行编程。在这种情况下,定时器中断也是由硬件直接注入到guest os上的。为了保持向后的兼容性,如果硬件支持,管理程序也会在SBI set_time调用中更新vstimecmp。因此,客户中较早的内核也利用了sstc的扩展。

38. RISC-V: KVM: Allow Guest use Svinval extension

We should advertise Svinval ISA extension to KVM user-space whenever
host supports it. This will allow KVM user-space (i.e. QEMU or KVMTOOL)
to pass on this information to Guest via ISA string.

RISC-V: KVM: Allow Guest use Zihintpause extension

We should advertise Zihintpause ISA extension to KVM user-space whenever
host supports it. This will allow KVM user-space (i.e. QEMU or KVMTOOL)
to pass on this information to Guest via ISA string.

Signed-off-by: Mayuresh Chitale <mchitale@ventanamicro.com>
Reviewed-by: Andrew Jones <ajones@ventanamicro.com>
Signed-off-by: Anup Patel <anup@brainfault.org>

RISC-V: KVM: Provide UAPI for Zicbom block size

We're about to allow guests to use the Zicbom extension. KVM
userspace needs to know the cache block size in order to
properly advertise it to the guest. Provide a virtual config
register for userspace to get it with the GET_ONE_REG API, but
setting it cannot be supported, so disallow SET_ONE_REG.

RISC-V: KVM: Expose Zicbom to the guest

Guests may use the cbo.inval,clean,flush instructions when the
CPU has the Zicbom extension and the hypervisor sets henvcfg.CBIE
(for cbo.inval) and henvcfg.CBCFE (for cbo.clean,flush).

Add Zicbom support for KVM guests which may be enabled and
disabled from KVM userspace using the ISA extension ONE_REG API.

Also opportunistically switch the other isa extension checks in
kvm_riscv_vcpu_update_config() to riscv_isa_extension_available().

39. RISC-V: KVM: Save mvendorid, marchid, and mimpid when creating VCPU

We should save VCPU mvendorid, marchid, and mimpid at the time
of creating VCPU so that we don't have to do host SBI call every
time Guest/VM ask for these details.

RISC-V: KVM: Add ONE_REG interface for mvendorid, marchid, and mimpid

We add ONE_REG interface for VCPU mvendorid, marchid, and mimpid
so that KVM user-space can change this details to support migration
across heterogeneous hosts.

增加 mvendorid marchid mimpid get_reg/set_reg qemu相关接口

总结

  1. 通用KVM模块所需的VCPU创建、启动和销毁功能

  2. 实现了VCPU中断和请求, 实现vcpu 设置 睡眠KVM_REQ_SLEEP/复位KVM_REQ_VCPU_RESET/休眠WFI 请求, 用户空间可以使用GET_MPSTATE/SET_MPSTATE ioctls来获取/设置VCPU的电源状态。

  3. KVM_GET_ONE_REG/KVM_SET_ONE_REG ioctls来访问读写VCPU 配置寄存器/通用寄存器/vcpu 控制和状态寄存器/浮点相关寄存器, 及mvendorid mimpid (version of the processor implementation) marchid的读取.

  4. 实现上下文切换时各寄存器状态的保存恢复, 包括上述的配置寄存器/通用寄存器/vcpu 控制和状态寄存器/浮点相关寄存器, 及host os的相关寄存器的保存恢复

  5. 实现 MMIO exit 加载/存储指令中提取MMIO 指令细节并将MMIO读/写转发到用户空间来实现MMIO读/写仿真, KVM内核模块将负责提取 guest page fault中的mmio读写指令细节, 将其传达给用户态虚拟机管理程序(如qemu), 并在恢复VCPU之前进行寄存器更新

  6. 为guest/虚拟机实现了VMID分配器/管理, 主要功能在VMID发生变化时,使用VCPU请求KVM_REQ_UPDATE_HGATP为每个guest VCPU强制更新HW Stage2 VMID

  7. 实现了为每个guest/虚拟机的stage2页表编程的所有必要功能。在高层次上,阶段2相关函数的流程与KVM ARM/ARM64实现相似,但阶段2页表格式与KVM RISC-V有很大不同

  8. sbi相关实现, guest 调用sbi ecall相关指令时, 需要陷入到 hypervisor中, 需要kvm 模块实现对应的sbi请求. 相关sbi 规范需支持 v01->v02->v03 的演化.

    除了GETCHAR和PUTCHART调用被转发到用户空间,几乎所有SBI v0.1调用都在KVM内核模块中实现, SBI v0.2的实验和vendor 扩展调用转发给用户空间

    SBI HSM扩展允许操作系统在任何时候开始/停止 harts。它还允许有序启动 hart,而不是随机启动。实施SBI HSM exntesion,并将vcpu 0指定为boot vcpu id。所有其他非零的非启动vcpus都应该由实现HSM扩展的操作系统提出。如果guest os 没有实现HSM扩展,那么只有单个vcpu可以被操作系统使用。

    实现 SBI v0.3规范中的SRST(系统重置)扩展, hsm暂停调用扩展

    实现hfence 扩展, 为未来的嵌套虚拟化做准备

  9. kvm中 hva->gpa 内存memslot 动态分配相关优化

  10. 为即将到来的AIA、PMU、Nested和其他虚拟化功能模拟更多的系统指令。增加了一个可扩展的系统指令仿真框架, CSR仿真框架也有在用户空间仿真CSR的规定,但这将只用于非常特殊的情况. 如AIA IMSIC CSR仿真在用户空间或vendor厂商特定的CSR仿真在用户空间

  11. 实现 sstc, 允许guest os直接对vstimecmp CSR进行编程,而不是通过hypervisor进行SBI调用来对下一个事件进行编程。在这种情况下,定时器中断也是由硬件直接注入到guest os上的。为了保持向后的兼容性,如果硬件支持,hypervisor也会在SBI set_time调用中更新vstimecmp.

虚拟化技术概述

虚拟机特性:

  • 指令模拟 (guest 和 host 可以是不同的isa)
  • 本地指令直接执行( guest 和 host isa是相同的)

系统资源虚拟化:

  • cpu虚拟化
  • 内存虚拟化
  • io 虚拟化

常见的虚拟机软件:

  • vmware
  • virtual box
  • xen
  • linux-kvm qemu

image-20240416111641703

Hypervisor Virtual Machine Manager(VMM) 的功能

  • 控制所有的系统资源(CPU 内存 网络 存储等)
  • 创建虚拟机并分配响应的资源
  • 管理虚拟机的生命周期

VMM 调度程序和操作系统的调度进程类似, 操作系统调度的基本单位是进程/线程, VMM调度的单位是虚拟处理器. 当虚拟cpu被调度到时, VMM调度程序负责将vcpu上下文装载到物理处理器上, 然后vcpu所对应的guest os 指令开始真正被执行. 当时间片用完或虚拟处理器主动让出, 调度程序被触发. 调度程序根据调度策略, 挑选下一个vcpu继续运行.
与操作系统一样, VMM的调度策略可以有多种, 如平均时间片策略, 或按vcpu的权重分配时间片进行调度等.
虚拟机之间可以通信, VMM需要实现对应的通信机制, 并向虚拟机提供对应的api(可以是时间通知, 共享内存等), 需要严格的安全权限检查.

虚拟机环境管理包括创建/删除/暂停/查询/迁移等, 由虚拟机管理软件和VMM 管理接口组成.

物理资源的管理

  1. 处理器管理
    包括系统启动检测获取所有物理处理器, 对每个处理器进行初始化, 如设置运行模式, 设置页表, 设置中断处理函数等; 将所有的处理器纳入调度序列, 由调度程序对其进行调度. 还支持hot plug, 当有处理器插入时, vmm获得通知, 将其纳入调度序列. 当处理器拔出时, vmm 得到通知, 将该处理器上执行的任务迁移到其他处理器上, 并将其从管理队列中删除.

云服务存在多个处理器节点, 当某些节点出现故障时, 其上运行的guest os 不会宕机, 而是转移到其他正常工作的cpu节点上;
同样为了安全的扩充处理器资源, 在不断电的基础上进行cpu的扩充进而降低系统负载, 对服务器来说也是必要的;
2. 内存管理
系统启动时VMM检测并获取所有内存, 对获得的内存进行初始化, 包括分页设置页表等; 提供内存分配的接口, 给虚拟机分配内存, 并且维护虚拟机物理地址和实际物理地址的映射关系
3. 中断管理
根据中断来源, 或直接处理, 或转发给guest os 处理
4. 系统时间维护
VMM 拥有和时间相关的硬件资源, VMM 负责维护系统时间, 同时向各guest os 提供虚拟化的时间
5. 设备管理
所有的外设都属于VMM, VMM需要包含所有设备的驱动程序. 在混合模型下, 大部分的外部设备属于guest os, 少部分的设备属于VMM

虚拟化的优点:

  • 更高的系统安全性和可用性
    • VMM 作为监视层, 运行在比os更高的特权层
    • 控制过滤虚拟机的行为
    • 监控虚拟机状态, 故障快速恢复
  • 最大化硬件资源使用率
    • 在一个物理主机上创建多个虚拟机共享主机资源, 节约硬件成本
  • 系统易扩展
    • 修改虚拟机的配置来适应业务的负载变化
    • Aggregation 聚合技术
  • 方便的可移植性
    • 虚拟机的系统消除物理主机的硬件差异
    • 虚拟机以文件镜像的格式封装
  • 硬件级别的隔离特性
    • 硬件沙盒机制

不同类型的VMM:

baremetal vmm (type1)

  • 启动时bootloader/BIOS 直接将执行权限交给hypervisor

  • 直接运行在硬件上, 不依赖基础操作系统

  • 可以控制所有的guest os

  • 交互少, 性能好, 稳定

  • 无法直接利用现有操作系统生态, 硬件兼容性差, 驱动开发工作量大

    VMM根据产品定位, 有选择的挑选一些io 设备来支持, 如面向服务器市场, 只会挑选服务器上的io设备来开发驱动, 另外调度和电源管理等的很多功能需要在VMM中重新实现

  • 典型代表: Xen
    image-20240416111647411

hosted vmm (type2)

  • 启动时bl/BIOS 先启动host os, hypervisor/VMM 相当于host os中跑的一个应用
  • 需要通过host os 来访问硬件资源
  • vmm 只能控制guest os, 不能控制host os中的其他部分
  • guest os 和 host os 交互调用链长, 影响性能
  • 攻击窗口多, 安全性差
  • 可以直接利用现有操作系统生态, 硬件兼容性好
  • 典型代表: linux kvm
    image-20240416111651919

KVM的思想是在Linux内核的基础上添加虚拟机管理模块,重用Linux内核中已经完善的进程调度、内存管理、IO管理等代码,使之成为一个可以支持运行虚拟机的Hypervisor

image-20240416111701026

混合模型

上述两种模式的集合体, VMM依然位于最底层, 拥有所有的物理资源, 与type1 模式不同的是, VMM会让出大部分io设备的控制权, 将它们交给guest os 控制, 相应的, VMM虚拟化的职责被分担, 处理器和内存的虚拟化仍由VMM 完成, 而IO虚拟化则由VMM和guest os 合作完成.

Hypervisor 的实现

没有硬件虚拟化技术之前,Hypervisor 的实现主要有两种,一种是半虚拟化,一种是全虚拟化

半虚拟化:

  • 将guest os 降权, 使其无法直接访问系统特权资源
  • vmm 提供访问系统特权资源的hyper call api
  • 修改guest os, 用hyper call api 访问系统特权资源
  • 高效轻量, 性能好
  • guest os 修改量大, 使用不便

全虚拟化:

  • 将guest os 运行在vmm 创建的独立环境里
  • vmm 将内核特权访问操作翻译成一系列对vmm 的请求 (软件方案)
  • guest os对虚拟化环境不感知, 不需要修改guest os
  • vmm 实现复杂
  • vmm和guest os 之间翻译产生的负载比较大, 性能差

硬件虚拟化 (简化hypervisor, 提高性能)

  • intel VT-x VT-d
  • AMD svm
  • ARM smmu等

CPU 虚拟化

虚拟CPU上下文

  • 类似于进程上下文的概念,当虚拟机发生退出时,需要保存虚拟CPU中各寄 存器的状态
  • 发生虚拟CPU调度时,需要保存当前虚拟CPU的上下文并加载待调度虚拟 CPU上下文

image-20240416111705202

软件方案

不同指令集架构上的软件解决方案

  • 解释执行

    解释器将程序二进制解码后调用指令相应的模拟函数

    采用软件模拟的方式逐条模拟虚拟机指令的执行

  • 二进制翻译

    区别于解释执行技术, 二进制翻译技术以基本块为单位, 将虚拟机指令批量 翻译后保存在代码缓存中

    基本块中的敏感指令会被替换为一系列其他指令

相同指令集架构上的软件解决方案

扫描与修补

x86 虚拟化技术早期, VMM 运行在特权级, guest os运行在非特权级(用户态), 这种方式称为特权级压缩(Ring Compression. guest os 上内核运行特权指令时, 通常会触发异常, 进入特权级, 由VMM 截获异常并进行处理, 但是有一些非特权敏感指令并不会触发异常, 这种状态下, VMM就需要扫描guest os的内核的所有的这些不会触发异常的敏感指令, 将其翻译成支持虚拟化的指令(会触发异常的指令), vmm 再去处理这些翻译后指令触发的异常.

修改系统资源的,或者在不同模式下行为有不同表现的,都属于敏感指令

硬件辅助方案

软件方案是非常低效的, 且vmm 实现过于复杂, Intel VT-x、AMD SVM、ARM EL2、RISC-V H-Extension, 加入了一层特权级别, guest os 运行在这一层上, 这一层上所有特权指令均会触发异常, 可以被VMM截获处理

image-20240416111708905

中断虚拟化

中断控制器的模拟

需要为每一个虚拟机维护一个虚拟中断控制器

image-20240416111712769

物理主机, 设备中断处理流程

  1. 设备产生一个水平/边缘触发中断信号
  2. 中断控制器响应该信号, 让cpu el1 进入中断异常模式
  3. os 调用中断服务程序, 通过irq number 找到对应驱动中断处理函数
  4. 完成中断处理

image-20240416111716987

虚拟化系统中, 设置中断处理流程

  1. 设备产生一个水平/边缘触发中断信号
  2. 中断控制器响应该信号, 让cpu el2 进入中断异常模式
  3. CPU 调用VMM 中断服务程序, 通过IRQ number 找到对应的guest os, 通过中断注入程序向guest os 注入virtual irq
  4. guest os el1 cpu进入中断异常模式
  5. guest os 调用中断服务程序, 通过virtual irq number 找到对应的驱动中断处理函数
  6. 完成中断处理
    image-20240416111722144

内存虚拟化

常见的内存虚拟化技术

  1. 地址空间分区
    • 简单, 但不灵活
  2. 半虚拟化 shadow page table
    • 虚拟地址模拟物理地址, 性能好, 需要修改guest os
  3. mmu 二级地址转换
    • 通过mmu硬件地址翻译
    • 对guest os 保持透明, 不需要修改原有mmu 代码
    • 运行时软件不介入翻译

硬件虚拟化- 二级地址翻译

实际上是扩展了影子页表技术, 硬件辅助化的措施: 新增寄存器,供硬件MMU读取, 由硬件mmu 完成二级页表翻译, 避免由软件翻译

  1. IPA (intermediate physical address) guest 物理地址
  2. 虚拟机物理地址空间由IPA 描述, 不直接指向真实物理地址
  3. 每个VM的IPA地址独立, 可以相同, 可以重叠, 也可以完全不同
  4. 通过两级地址翻译找到真实物理地址
    1. VA->IPA (guest os)
      arm64 使用TTBRn_EL1寄存器和页表, riscv 使用stap寄存器
    2. IPA->PA (VMM)
      arm64 使用VTTBR_EL2寄存器和stage2 页表, riscv 使用hgatp 寄存器
      img

各指令集架构内存硬件虚拟化辅助新增的寄存器

Intel:EPT,起始地址:EPTP(in VMCS)

AMD:NPT

ARM:Stage-2 页表,起始地址:VTTBR_EL2

RISCV: Stage-2 页表,起始地址:hgatp

IO 模拟

在没有虚拟化的系统中,由BIOS或者操作系统通过遍历PCI总线上的所有设备完成设备发现过程,而在虚拟化系统中,则由VMM决定向虚拟机呈现哪些设备。具体过程要根据设备是否存在于物理总线上来进行。对于一个真实存在于物理总线的设备,如果是不可枚举的类型,例如PS/2键盘,由于这类设备是硬编码固定的,驱动程序会通过其特定的访问方式来检查设备是否存在,因此VMM只要在相应端口上模拟出该设备,虚拟机即可成功检测到它;如果是可枚举的类型,譬如PCI设备或者PCIe设备,这种设备通常定义了完整的设备发现方法,并允许BIOS或者操作系统在设备枚举过程中通过PCI配置空间对其资源进行配置。因此VMM不仅要模拟这些设备本身的逻辑,还要模拟PCI总线的一些属性,包括总线拓扑关系及相应设备的PCI配置空间,以便虚拟机OS在启动时能够发现这些设备

所谓设备模拟就是模拟设备的功能,内容十分多样且复杂。对于像PS/2键盘、鼠标这样的设备,VMM需要根据设备的接口规范模拟设备的所有行为,才能够无需修改驱动就在虚拟机上展现出设备应有的效果。而对于磁盘存储系统,则不必受限于实际的磁盘控制器以及具体磁盘类型和型号。比如,对IDE硬盘其I/O端口虚拟化时,底层可以是一块磁盘,可以是一个分区,也可以是不同格式的文件; 然后在其上实现一个专门的块设备抽象层; 最后在块设备上使用文件系统,并引入一些真实硬件没有的高级特性,例如:加密、备份、增量存储等。

系统的物理设备需要同时接受来自多个虚拟机的I/O请求。因此,VMM还要将多个虚拟机的I/O请求合并为单独一个I/O数据流发送给底层设备驱动。当VMM收到来自底层设备驱动完成I/O请求的中断时,VMM还要能够将中断响应结果转发给正确的虚拟机,以通知其I/O操作结束。同时VMM在调度各个虚拟机发送来的I/O请求处理时,必须依据一定的算法确保虚拟机I/O的QoS与设备共享的公平性。

对于type2 类的拥有完整生态的操作系统, vmm 作为一个内核模块, 客户机发生io后, vmm将其拦截后, 会通过用户态接口传给用户态的设备模型处理, 设备模型运行在host os 上的用户态层, 可以使用相应的系统调用及运行库. host os及运行库, 构成了设备模型的运行环境

image-20240416111754667

而对于type1 类的baremental os, 通常不存在用户态, 设备模型是位于虚拟机设备驱动程序和实际设备驱动之间的一个模块, 由guest os发过来的io请求先通过设备模型模块转化为物理io设备请求, 在通过调用物理设备驱动来完成相应的io操作. 真实的设备驱动将io操作结果通过设备模型模块返回给guest os的虚拟设备驱动.

image-20240416111758642

  • type2 类 用户态设备模型:

    • QEMU/KVM

    • 用户态设备模型, 运行库生态健壮, 可复用性高

    • 多次上下文切换

  • type1 类 baremental 设备模型:

    • xvisor xen

    • 减少了多次上下文切换

    • 缩短io模拟路径

    • 移植性差

设备类型

  1. 端口io
    通过特定指令访问设备相关寄存器 (x86 专用的端口访问指令,IN/OUT)
  2. MMIO
    特定物理内存区域映射了设备的寄存器, os通过页表以访问内存的方式访问设备寄存器, RISCV 仅支持MMIO
  3. DMA
    无需CPU控制, DMA控制器接管地址总线
  4. PCI
    或端口io方式或 MMIO方式,不占用固定地址-> 热插拔

IO 虚拟化基本任务

  • 访问截获
  • 提供设备接口
    • 虚拟设备接口, 如暴露虚拟pci 设备
    • 直通设备接口, 如intel VT-d 技术
    • 对虚拟机完全“透明”
  • 实现设备功能
    • type1 类baremental VMM 需要实现设备驱动, 设备模型;
    • type2 类需要实现用户态设备模型 运行库等

端口IO 模拟

虚拟机陷入过程

  • IN/OUT、INS/OUTS -> VM-Exit
  • 保留端口号、访问数据宽度、数据传输 方向、数据传输方向

VMM 处理过程:

  • I/O端口对应的处理函数在设备模型初始 化时会被注册到hypervisor中
  • 函数指针被组织成数组
  • 根据I/O端口号和访问数据宽度寻找相应 端口IO处理函数

image-20240416111803993

MMIO模拟过程

虚拟机陷入过程

  • 访存指令,非特权指令
  • 页表不存在相应页表项 -> 缺页异常 -> 陷入VM

Hypervisor中的处理

  • MMIO内存区域较大,通常不采用PIO中的函数数组形式
  • 为MMIO区域注册一个MMIO处理函数
  • 处理函数定位到需要访问的I/O端口

DMA 模拟

  • guest 驱动程序配置DMA 相关寄存器, 源地址 目的地址 (GPA) 及 长度
  • 陷入VMM, 设备模型用VMM 提供的内存管理功能 将源地址目的地址 (GPA) 翻译为 HPA, 配置物理DMA 的地址寄存器为HPA, 同时需要建立HPA 与 HVA的映射关系, 对其进行占位, 防止别的进程或vcpu把这块物理内存给抢走. 这个地方纯软件实现比较复杂, 涉及到映射给 guest 的虚拟设备地址与真实设备地址之间的转换, 如果为memory->memory, 会简单一些
  • guest 客户机驱动程序通过配置虚拟DMA的 命令控制寄存器发起 DMA操作
  • 陷入VMM, 设备模型截获这个操作后, 配置物理DMA命令控制寄存器
  • DMA 自行在HPA 间搬运数据
  • DMA 搬运完毕后, 通过中断通知vmm 设备模型, vmm设备模型返回到guest os中, DMA 请求结束

设备直通

软件实现I/O虚拟化的技术中,所有的虚拟机都共享物理平台上的硬件设备。如果物理条件好,有足够的硬件,就可以考虑让每个虚拟机独占一个物理设备,这样无疑会提高系统的性能。把某一个设备直接分配给一个虚拟机,让虚拟机可以直接访问该物理设备而不需要通过VMM或被VMM截获,这就是设备直通技术。

intel 的 VT-d与AMD的IOMMU技术 arm的smmu 技术。尽管这两种技术在一定程度上提高了I/O访问性能,但代价却是限制了系统的可扩展性

  • 不发生或发生少量陷入VM
  • 性能接近裸机

ARM 硬件虚拟化技术:

CPU 特权层扩展

  • EL2 层(arm64) / HYP (arm32) 模式下运行vmm
  • guest os 运行在原有的特权模式, 不需要修改guest os
  • vmm所在层权限更高, 可以控制guest os 访问硬件的权限

image-20240416111808663

模式切换

虚拟机 → Hypervisor

- EL1 → EL2 
- 敏感指令触发(可通过HCR_EL2寄存器细粒度 控制)

Hypervisor → 虚拟机

  • EL2 → EL1
  • eret指令触发

上下文切换

  • EL1与EL2各自有一套系统寄存器
  • 虚拟CPU调度时,需要将原虚拟CPU系统寄 存器保存至内存并从内存中加载目标虚拟 CPU寄存器

因为有了硬件虚拟化的支持,所以hypervisor的实现 基本是基于硬件的 trap 和 软件的emulator 来实现的。guest os 访问一些特权寄存器或者指令,会进到 hypervisor ,然后会调用特权寄存器的访问函数来访问特权寄存器。如果是要访问硬盘,或者网络,会通过io 模拟器,来访问具体的模拟器。

image-20240416111813128

CPU的虚拟化,就是让多个Guest os 分时的运行在同一个CPU上,都有自己独立的物理地址空间,让 hypervisor在EL2 层来帮助多个VM 来进行上下文的切换,这个和linux 进程的概念非常的相似,不过保存的上下文寄存器不一样,这里有两个重要的寄存器,HCR_EL2ESR_EL2。HCR_EL2 是用来配置VM的参数,就是产生trap的条件,什么情况下会产生trap , 什么情况下不会产生trap,右边是一个运行两个VM的例子。WFI指令是说明自己工作做完了,是idle状态了。

image-20240416111820943

ARM CPU虚拟化通过硬件trap和软件模拟完成

  1. HCR_EL2 hyper配置寄存器
    1. 配置vm产生硬件trap的条件
    2. 有非常丰富的组合, 如TLB/cache的操作, 一些特殊指令
  2. ESR_EL2 异常寄存器
    1. 当trap发生时, 确定vm产生硬件trap的原因

执行特权指令示例:

与特权级无关的一般的指令和通用寄存器在任何特权级都可以任意执行。而每个特权级都对应一些特殊指令和 控制状态寄存器 (CSR, Control and Status Register) ,来控制该特权级的某些行为并描述其状态。当然特权指令不只是具有有读写 CSR 的指令,还有其他功能的特权指令。

如果低优先级下的处理器执行了高优先级的指令,会产生非法指令错误的异常,于是位于高特权级的执行环境能够得知低优先级的软件出现了该错误, 进而陷入到高特权级处理该指令

image-20240416111824532

访问特定寄存器示例:

image-20240416111828304

使用陷入来虚拟化操作需要大量计算。比如功能寄存器ID_AA64MMFR0_EL1,不经常被操作系统访问。当将对这些寄存器的访问捕获到虚拟机监控程序中以模拟读取时,计算是可以接受的。
但是对于访问频率高的寄存器,比如MPIDR_EL1,或者在性能关键代码中,需要尽可能地优化陷入,对这些寄存器,ARM提供了其它策略,hypervisor可以在进入VM时先配置好这些寄存器的值。例如,当VM中读到MPIDR_EL1时会自动返回VMPIDR_EL2的值而不发生陷入

MMU虚拟化支持

  • LPAE(arm32 大地址拓展技术) , stage2 translation
  1. 物理直通设备MMIO

    • 阶段-2页表包含物理空间地址 与虚拟机IPA之间的映射

    • 不触发VM-Exit

  2. 虚拟设备MMIO

    • 阶段-2 缺页异常

      • 将IPA地址填充到HPFAR_EL2寄存器中
      • 访问相关信息[Read,4 bytes, x0],填充到ESR_EL2寄存器中
    • 调用emulate_access函数,完成 MMIO的模拟

    • ERET指令将控制流返回给vCPU

image-20240416111832268

GIC 虚拟化

  • vcpu interface, hypervisor interface

虚拟CPU接口直接访问

  • 将CPU接口置于CPU内部,通过寄存器访问
  • 为虚拟CPU接口提供专用寄存器(ICV_*),区别于物理CPU接口
  • HCR_EL2.IMO和HCR_EL2.FMO设置为1时,运行在EL1中的Guest OS对CPU接口系 统寄存器(ICC_*)的访问将被重定向到相应的ICV_*寄存器而不会触发虚拟机陷入

虚拟中断注入

  • GICv3配置使得所有的物理中断路由到EL2
  • Hypervisor检查中断目标是否为vCPU
  • 根据ICH_LRn_EL2寄存器中保存的虚拟中断信息向虚拟机中注入中断

虚拟LPI中断直接注入

  • GICv3引入组件中断翻译服务ITS,该组件通过查询若干内存表将物理中断转化为 虚拟LPI中断注入相应的虚拟机

arch_timer 虚拟化

  • hypervisor timer, virtual timer

时钟虚拟化要为guest os提供两种设备资源:

  • 计时资源:clock_source设备
  • 时钟定时器: clock_event设备

对于clock_event设备,ARM64平台每个处理器有4个timer,3个物理timer和1个虚拟timer。物理机用物理timer,虚拟机用虚拟timer,相互之间并无资源冲突

对于clock_source设备,按照ARM64的timer的设计,有物理counter和虚拟counter,物理机使用物理counter,虚拟机使用虚拟counter

smmu

  • stage2 translation for DMA

  • 一种DMA重映射机制

  • 扩大设备DMA寻址范围, 当系统无法提供大块连续物理内存时,也可以通过SMMU转换让设备可以访问分散物理内存

  • IOMMU在ARM-V8架构下的解决方案,与VT-d类似

  • SMMU与MMU共用一套阶段-2页 表

  • 设备直通

    • 设备透传就是由虚机直接接管设备,虚机可以直接访问MMIO空间,VMM配置好IOMMU之后,设备DMA读写请求也无需VMM介入
  • 为每个虚拟机划定可用的设备, 起到隔离保护作用

    image-20240416111836599

物理机DMA 示例
image-20240416111840735

  • 网卡模块将数据从物理地址填入网卡DMA 寄存器
  • DMA模块通过物理地址访问物理内存

虚拟机网卡如何进行DMA?
image-20240416111845117

  • 网卡驱动将数据的guest 物理地址填入网卡DMA 寄存器

  • 通过IPA 访问总线会产生故障

    1. 调用hypervisor api 直接转换VA->PA
      缺点: 修改驱动, API 调用产生额外性能开销
    2. 为外设添加额外虚拟化支持
      不需要修改驱动, 驱动无感知, 无额外api调用性能开销
      SMMU 为系统除CPU 之外的任何具有DMA 能力的设备提供地址翻译服务和保护功能
      • PCIE DMA 设备
      • platform DMA 设备
      • GPU/VE 加速器

    image-20240416111848614

SMMU for DMA 概述

流表:每个流表项对应一个设备

  • VMID:设备所属虚拟机
  • S2TTB:阶段-2转换页表基地址
  • S1上下文指针:指向阶段-1上下文描述符表(CD 表)

CD表:

  • TTB0 TTB1:分别保存用户空间和内核空间阶段-1页表
  • ASID:用于标记进程的地址空间

SMMU从I/O 事务中获取设备标识符,即 StreamID

SMMU从SMMU_STRTAB_BASE寄存器中获取流表的基地址,并通过StreamID获取对应的STE(流表项)

在开启阶段-1转换的情况下,通过SubstreamID 定位到对应的CD,进而获取ASID和阶段-1页表 基地址。在开启阶段-2转换的情况下,在STE 中获取VMID和阶段-2页表基地址以及Stream World配置信息

SMMU根据DMA地址、ASID、VMID、Stream World 查 询TLB。如果TLB命中,可以直接获得目标物理地址以及访问权限信息。如果TLB未命中,通过相应地址翻译过程获得对应的目标物理地址, 并将映射关系填充到TLB中

设备根据目标物理地址进行数据传输

image-20240416111852573

RISCV 虚拟化技术

已有的RISC-V虚拟化软件实现

目前已有的实现有Xvisor和KVM,Xvisor是1类虚拟化软件,而KVM属于2类。

RISC-V规范定义了RISC-V H-extension,在原来的3级特权架构的基础上,对原有的Supervisor模式进行了扩展,引入了Hypervisor-Extended Supervisor mode (HS)

虚拟化H扩展定义了一个硬件状态位,称作V状态,可以为0或1,V状态不同,定义和访问的CSR寄存器也不同。

  • 当V为0时
    • 以“s”开头的CSR寄存器表示当前操作系统的状态
    • “hs”开头的用于支持和实现虚拟化软件
    • “vs”开头的代表运行在虚拟化技术上的系统状态。
  • 当V为1时
    • “s”开头的寄存器指向了前文以“vs”开头的寄存器。

image-20240416111856820

模式切换

  • 虚拟机 → Hypervisor
    • VS mode → HS mode(先进入M mode,再由M mode 转发给HS mode)
    • 敏感指令触发
  • Hypervisor → 虚拟机
    • HS mode → VS mode
    • sret指令触发

上下文切换

  • 为VS-mode提供VS CSR
  • 虚拟CPU调度时,同样需要从内存中保存和加载相应 的寄存器

image-20240416111900776
image-20240416111905467
In HS-mode (V=0):

  • s 开头的寄存器含义不变
  • h开头的寄存器指示hypervisor 的能力
  • vs 开头的寄存器指示vs 模式下的状态 (context switch时使用)
    VS-mode(V=1):
  • s开头的寄存器指向 vs 开头的寄存器

2级mmu 地址转换

使用stap 与hgatp 寄存器完成2级 地址转换, 原理一样
va->ipa (guest os) satp
ipa -> pa (host os) hgatp

中断

hedeleg 虚拟异常代理寄存器

hideleg 虚拟中断代理寄存器

默认状态下, 在各级代理寄存器未设置时, 所有的trap 和 中断都被指向到 M 模式的trap (即mtvec 指定的入口函数处), 在指定了 medelegmdieleg后, 相应bit位的trap 和 中断 指向到 HS 模式的trap (即stvec 指定的入口函数处), 进一步, 在指定了 hedeleghideleg 后, 相应bit位的trap 和 中断指向到 VS 模式下的trap(即vstvec 指定的入口函数处)

image-20240416111912801

hedeleg 中, 9-11 bit, 20-23 bit 是readonly的, 只能是0

第0, 3, 8, 12, 13, 15 为推荐设置bit

image-20240416111916682

hideleg 中, 0-15 中, 只有10, 6, 2 能被设置. 当hideleg中, bit 10被设置后, 10号中断来了后, 被代理到VS-mode后, code 10 会被自动转换为 code 9; 同样的 6号中断被自动转换为 5, 2号中断被自动转换为 1号. 这样做的目的是为guest os 中的 kernel 不用进行额外的修改来适配虚拟机.

除此之外, HS模式可以使用hvip寄存器,来向VS模式注入虚拟的中断。

Hypervisor Status Register (hstatus)

image-20240416111920612

When VTSR=1, an attempt in VS-mode to execute SRET raises a virtual instruction exception.
When VTW=1 (and assuming mstatus.TW=0), an attempt in VS-mode to execute WFI raises a virtual instruction exception if the WFI does not complete within an implementation-specific, bounded time limit.
When VTVM=1, an attempt in VS-mode to execute SFENCE.VMA or to access CSR satp raises a virtual instruction exception.

The VGEIN (Virtual Guest External Interrupt Number) field selects a guest external interrupt source for VS-level external interrupts. VGEIN is a WLRL field that must be able to hold values between zero and the maximum guest external interrupt number (known as GEILEN), inclusive.
When VGEIN=0, no guest external interrupt source is selected for VS-level external interrupts. GEILEN may be zero, in which case VGEIN may be hardwired to zero.
Guest external interrupts are explained in Section 1.2.4, and the use of VGEIN is covered further in Section 1.2.3.

Field HU (Hypervisor User mode) controls whether the virtual-machine load/store instructions, HLV, HLVX, and HSV, can be used also in U-mode. When HU=1, these instructions can be executed in U-mode the same as in HS-mode. When HU=0, all hypervisor instructions cause an illegal instruction trap in U-mode.

hip & hie

Registers hip and hie are HSXLEN-bit read/write registers that supplement HS-level’s sip and sie respectively. The hip register indicates pending VS-level and hypervisor-specific interrupts, while hie contains enable bits for the same interrupts. As with sip and sie, an interrupt i will be taken in HS-mode if bit i is set in both hip and hie, and if supervisor-level interrupts are globally enabled.

image-20240416111924324

Bits hip.SGEIP and hie.SGEIE are the interrupt-pending and interrupt-enable bits for guest external interrupts at supervisor level (HS-level).
SGEIP is read-only in hip, and is 1 if and only if the bitwise logical-AND of CSRs hgeip and hgeie is nonzero in any bit.

Bits hip.VSEIP and hie.VSEIE are the interrupt-pending and interrupt-enable bits for VS-level external interrupts. VSEIP is read-only in hip, and is the logical-OR of these interrupt sources:

  • bit VSEIP of hvip;
  • bit of hgeip selected by hstatus.VGEIN
  • any other platform-specific external interrupt signal directed to VS-level.

Bits hip.VSTIP and hie.VSTIE are the interrupt-pending and interrupt-enable bits for VS-level timer interrupts. VSTIP is read-only in hip, and is the logical-OR of:

  • hvip.VSTIP
  • any other platform-specific timer interrupt signal directed to VS-level.

Bits hip.VSSIP and hie.VSSIE are the interrupt-pending and interrupt-enable bits for VS-level software interrupts. VSSIP in hip is an alias (writable) of the same bit in hvip.

Hypervisor Guest External Interrupt Registers (hgeip and hgeie)

hgeip indicates pending guest external interrupts for this hart.
hgeie contains enable bits for the guest external interrupts at this hart.
Guest external interrupts represent interrupts directed to individual virtual machines at VS-level.
只有在设备直通场景下使用

interrupts from the device are intended for a specific virtual machine. Each bit of hgeip summarizes all pending interrupts directed to one virtual hart, as collected and reported by an interrupt controller. To distinguish specific pending interrupts from multiple devices, software must query the interrupt controller.

Support for guest external interrupts requires an interrupt controller that can collect virtual-machine-directed interrupts separately from other interrupts.

中断控制器需要收集virtual guest external interrupt, 和 物理cpu的external interrupt 区分开, guest 只能接收virtual external guest interrupt

The number of bits implemented in hgeip and hgeie for guest external interrupts is GEILEN (hstatus.VGEIN)
bits GEILEN:1 shall be writable in hgeie, and all other bit positions shall be hardwired to zeros in both hgeip and hgeie.

Guest external interrupt number i at one physical hart is typically expected not to be the same as guest external interrupt i at any other hart.
For any one physical hart, the maximum number of virtual harts that may directly receive guest external interrupts is limited by GEILEN.

A hypervisor is always free to emulate devices for any number of virtual harts without being limited by GEILEN. Only direct pass-through (direct assignment) of interrupts is affected by the GEILEN limit, and the limit is on the number of virtual harts receiving such interrupts, not the number of distinct interrupts received. The number of distinct interrupts a single virtual hart may receive is determined by the interrupt controller.

The enable bits in hgeie do not affect the VS-level external interrupt signal selected from hgeip by hstatus.VGEIN.

vsip & vsie

hideleg not zero, vsip.SEIP and vsie.SEIE are aliases of hip.VSEIP and hie.VSEIE.
vsip.STIP and vsie.STIE are aliases of hip.VSTIP and hie.VSTIE.
vsip.SSIP and vsie.SSIE are aliases of hip.VSSIP and hie.VSSIE.

小结

hideleg 开启时,

  • vsip.SEIP = hip.VSEIP vsip.STIP = hip.VSTIP vsip.SSIP = hip.VSSIP
    • VSEIP is read-only in hip, and is the logical-OR of these interrupt sources:
      • bit VSEIP of hvip;
      • bit of hgeip selected by hstatus.VGEIN
      • any other platform-specific external interrupt signal directed to VS-level.
    • VSTIP is read-only in hip, and is the logical-OR of:
      • hvip.VSTIP
      • any other platform-specific timer interrupt signal directed to VS-level.
    • VSSIP in hip is an alias (writable) of the same bit in hvip.
      • 即 vsip.SSIP = hip.VSSIP = hvip.VSSIP
  • vsie.SEIE = hie.VSEIE vsie.STIE = hie.VSTIE vsie.SSIE = hie.SSIE

真正影响guest os的是vsip , guest os 运行时处于V=1 mode, vsip被替换为sip,

  • vsip software 中断来自于hvip (来自于HS hypervisor), 或者guest os 自己.
  • vsip timer 中断来自于hvip (来自于 HS hypervisor), 或guest os 自己, 或 platform-specific timer interrupt signal directed to VS-level
  • vsip guest external 中断来自于hvip (来自于HS hypervisor), 或hstatus.VGEIN -> hgeip 或 any other platform-specific external interrupt signal directed to VS-level

下面这两句应该怎么理解?

  • Bits hip.SGEIP and hie.SGEIE are the interrupt-pending and interrupt-enable bits for guest external interrupts at supervisor level (HS-level).
  • Bits hip.VSEIP and hie.VSEIE are the interrupt-pending and interrupt-enable bits for VS-level external interrupts.

直译:
SGEIP 和 SGEIE 是给HS 用的, 重点看 SGEIP, 这个是hgeip & hgeie and的结果, 代表的有guest external 中断待处理.
hip.VSEIP 和 hip.VSEIE 是给VS 用的, 这个好理解, 这两个直接反应到vsip的相应bit上了.

结合这句:
if GEILEN is nonzero, bits GEILEN:1 shall be writable in hgeie
Only direct pass-through (direct assignment) of interrupts is affected by the GEILEN limit, and the limit is on the number of virtual harts receiving such interrupts, not the number of distinct interrupts received

大概猜测, 在设备直通场景时, 0 - hstatus.VGEIN - GEILEN(物理cpu上托管的vcpu的数量), 不同的vcpu可以运行不同guest os.
一个物理cpu 有三套寄存器

  • v 开头的 vsip vsie 等
  • h 开头的 hvip hip hie hgeie hgeip 等
  • s 开头的 sip sie 等
    当V=0 -> V=1 时, v开头的寄存器会替换成 s 开头的寄存器, 此时变成vcpu的执行环境
    V=1 时, 只能访问 s 开头的寄存器.

需要HS vmm 对 hgeie hstatus操作, 对应物理cpu上托管的vcpu
如当前物理cpu上托管了8个vcpu, 正在运行的是第2个vcpu

  • 对 hstatus.VGEIN 设置为2
  • 对 hgeie 的前8个bit 置1, 表示物理cpu 管了8个vcpu, 这8个vcpu都要处理guest external interrupt.

在前面前提下, 硬件需要将hip.VSSIP 与 hgeip的状态区分.
前面hip.VSSIP 的来源处说的比较模糊: bit of hgeip selected by hstatus.VGEIN

不太好理解, 大概猜测:
PLIC 在设备直通场景(设置了hstatus.VGEIN时), guest 外部中断源需要区分给哪个vcpu, 导致的直接结果就是要设置hgeip 的哪个bit, 同时硬件应该将 hip.VSEIP 置为hgeip 与 hstatus.VGEIN 逻辑与 的结果. 而hip.SGEIP 置为 hgeip & hgeie 逻辑与的结果.

虚拟机接管, 因为vsip.SEIP = hip.VSEIP, 假如中断控制器要发给第三个vcpu, 就需要将hgeip 的第三个bit 置1, 则hip.VSEIP = 0, 而 hip.SGEIP 为 1, 表明有待处理的guest external interrupt.

在第二个vcpu 因tick到期 退出到hypervisor vmm 后, vmm 需要check hip.SGEIP, 此时有待处理的虚拟外部中断, 进而查hgeip, 查到是第三个vcpu的, 则切换到第三个vcpu 运行, 切换前将hstatus.VGEIN 设置为3. 此时vsip.SEIP = hip.VSEIP 会被置1, 第三个vcpu 陷入V=1 mode, 处理虚拟外部中断. 如guest os kernel 将sie.SEIE 置过位, 则guest os 会处理guest external 中断(10号中断会换成9号中断), guest os 需要查询plic 中断控制器, 判断外部中断是谁的, 该由谁的中断处理函数处理. 处理完后将sip.SEIP 清0, 返回到 hypervisor vmm 后, 因为 sip.SEIP -> vsip.SEIP = hip.VSEIP 同样已经被置0了.

备注

hgeip read-only csr, 由硬件操作
hgeie rw csr, 由软件操作
hip.SGEIP read-only bit, 由硬件操作
hip.VSEIP read-only bit, 由硬件操作

陷入模拟

为了虚拟数据吞吐,HS模式可以使用“陷入-模拟”法。即在访问内存 映射外设对应的地址时,产生相应的中断,通过模拟外设的运行来实现后续的过程。这种方式可以模拟PLIC外设、VirtIO外设和其它一些软件模拟的吞吐外设。

RISC-V可以通过核的CSR寄存器注入中断,因此不需要为虚拟化而特殊设计中断控制器外设。RISC-V的时钟和核间中断可通过SBI软件辅助完成,而Aarch64需要特殊设计的计时器外设来支持虚拟化功能。

不足

RISC-V对虚拟化的支持仍然只集中在CPU的虚拟化上。H-extension已经实现了与KVM和QEMU中的Xvisor的功能完备性。然而,目前还没有任何硬件实现是公开的,而且在可预见的未来,具有硬件虚拟化支持的商业RISC-V内核也不会被发布。RISC-V在虚拟化方面仍有一些差距。

  • 在ISA层面上,需要cache管理操作这样的功能。

    • 尽管现有的管理程序层提供了逻辑上的CPU和内存隔离,但由于虚拟机(VM)之间共享的微架构资源(如最后一级的缓存、互连和内存控制器)造成的相互干扰,恶意的虚拟机可以通过增加对共享资源的消耗来实施拒绝服务(DoS)攻击, 或者利用现有的定时侧信道来间接访问其他虚拟机的数据 (Intel Cache Allocation Technology)
  • 缺少对虚拟化至关重要的组件是IOMMU。需要一个IOMMU来实现高效的虚拟化,通过允许直接分配DMA能力的设备给虚拟机,同时保证虚拟机和管理程序本身之间的强隔离。提供设备直通的能力. 这方面最近才有正式的文档发布, 距离开源实现还需要一些时间.

    https://github.com/riscv-non-isa/riscv-iommu/releases

    image-20240416111929646

Xvisor RISCV

image-20240416111935167

Xvisor 上下文保存/恢复

image-20240416111938947

image-20240416111942274

Xvisor 中断处理流程

image-20240416111948496

Xvisor mmio 陷入模拟

image-20240416111952904

ARM Vs RISCV 硬件虚拟化辅助

image-20240416111957015

zicbom 提供的 cache 指令

Zicbom 指令 作用
CBO.FLUSH l1 & l2 cache 内容刷新到主存, 并将 cache line 状态置为无效, 下次访问该地址时, 会从主存 load 回来
CBO.CLEAN cache line 处于修改状态, 会将 cache line 的内容刷新回主存, 不改变该 cache line 的状态
CBO.INVAL 将 cache line 置为无效, 再次访问地址时, 会从主存上 load 回来

mcache

缓存操作用于控制操作,如初始化、失效、驱逐等。以下是缓存操作的简要描述:

  1. 索引写回失效(Index Writeback Invalidate):如果指定索引处的缓存行状态为有效且脏,该行将被写回到由缓存标记指定的内存地址。完成该操作后,缓存行的状态将设置为无效。如果行有效但不脏,则将行的状态设置为无效。
  2. 命中失效(Hit Invalidate):如果缓存包含指定地址,该缓存行的状态将设置为无效。
  3. 命中写回失效(Hit Writeback Inv):如果缓存包含指定地址且该行有效且脏,该行的内容将写回到主存。完成该操作后,缓存行的状态将设置为无效。如果行有效但不脏,则将行的状态设置为无效。
  4. 命中写回(Hit Writeback):如果缓存包含指定地址且该行有效且脏,该行的内容将写回到主存。完成操作后,行的状态保持有效,但脏状态被清除。
  5. 获取并锁定(Fetch And Lock):如果缓存包含指定地址,锁定该行。如果缓存不包含指定地址,从主存重新填充该行,然后锁定该行。 (只对 l2 cache 有效)

对比

对应于 cbo 指令:

  • mcache Hit Invalidate 与 CBO.INVAL 等效
  • mcache Hit Writeback 与 CBO.CLEAN 等效
  • mcache Hit WriteBack Inv 与 CBO.FLUSH 等效

不同:

  1. cbo 的三条指令针对的是所有 cache (l1 l2)等,
    mcache 的这三条指令粒度更细, 需要分别指定 l1 和 l2 进行操作
    如:
    Hit Writeback 有 Hit_Writeback_D 和 Hit_Writeback_S 两个操作 type, 分别用来 writeback l1 cache 和 l2 cache

  2. CBO 的三条指令是可以在 S-mode 及 M-mode 下执行的
    而 mcache 指令只能在 M-mode 下执行, S-mode 需要 cache 相关操作只能通过陷入到 M-mode 的 opensbi 来完成.

  3. mcache 另外提供了 index 操作, 主要方便用来回写并清空所有 l1 l2 cache line(不需要知道具体地址, 只需要按 index 覆盖 cacheline 内存范围即可)
    另外还提供了 Index invalidate icache Hit invalidate icache 和 fill icache 以及 fetch & lock l2 cache 的行为.

https://github.com/mit-pdos/xv6-riscv/
一个简单,类UNIX的 MIT(麻省理工) 教学用操作系统

用户态 syscall

Xv6内核提供了Unix内核传统上提供的服务和系统调用的子集

系统调用 描述
int fork() 创建一个进程,返回子进程的PID
int exit(int status) 终止当前进程,并将状态报告给wait()函数。无返回
int wait(int *status) 等待一个子进程退出; 将退出状态存入*status; 返回子进程PID。
int kill(int pid) 终止对应PID的进程,返回0,或返回-1表示错误
int getpid() 返回当前进程的PID
int sleep(int n) 暂停n个时钟节拍
int exec(char *file, char *argv[]) 加载一个文件并使用参数执行它; 只有在出错时才返回
char *sbrk(int n) 按n 字节增长进程的内存。返回新内存的开始
int open(char *file, int flags) 打开一个文件;flags表示read/write;返回一个fd(文件描述符)
int write(int fd, char *buf, int n) 从buf 写n 个字节到文件描述符fd; 返回n
int read(int fd, char *buf, int n) 将n 个字节读入buf;返回读取的字节数;如果文件结束,返回0
int close(int fd) 释放打开的文件fd
int dup(int fd) 返回一个新的文件描述符,指向与fd 相同的文件
int pipe(int p[]) 创建一个管道,把read/write文件描述符放在p[0]和p[1]中
int chdir(char *dir) 改变当前的工作目录
int mkdir(char *dir) 创建一个新目录
int mknod(char *file, int, int) 创建一个设备文件
int fstat(int fd, struct stat *st) 将打开文件fd的信息放入*st
int stat(char *file, struct stat *st) 将指定名称的文件信息放入*st
int link(char *file1, char *file2) 为文件file1创建另一个名称(file2)
int unlink(char *file) 删除一个文件

用户态的基础接口

  • 文件操作 open read write close link unlink stat fstat mkdir chdir
  • 进程相关 fork dup wait pipe getpid exit kill
  • log相关 printf
  • 内存 malloc free
  • 时间相关 uptime(获取当前的tick) sleep

文件系统

提供了一个精简版的文件系统, 支持ramdisk, 支持文件读写

可改code, 将用户态程序打包进文件系统, 将文件系统镜像融入到 kernel, 使之成为 kernel .rodata段的内容, 作为ramdisk使用

不依赖加载器, 可以将用户态应用程序和 little kernel 打包到一起

cpu mode

从M-mode 启动到 S-mode 的xv6 little kernel, 最后启动到 U-mode 的sh 终端

smp

支持多核启动

内存布局

启用mmu

kernel 为线性映射 VA 等同于 PA

Xv6 为每个进程维护一个用于描述进程的用户地址空间的页表,外加一个单独的描述内核地址空间的页表。内核配置其地址空间的布局,使其能够通过可预测的虚拟地址访问物理内存和各种硬件资源。

image-20230222143152947

​ 内核地址空间

内核对RAM和内存映射的设备寄存器使用“直接映射”,也就是将这些资源映射到和它们物理地址相同的虚拟地址上。例如,内核本身在虚拟地址空间和物理内存中的位置都是KERNBASE=0x80000000。直接映射简化了读/写物理内存的内核代码。例如,当 fork 为子进程分配用户内存时,分配器返回该内存的物理地址;fork 在将父进程的用户内存复制到子进程时,直接使用该地址作为虚拟地址。

有几个内核虚拟地址不是直接映射:

  • trampoline 页。它被映射在虚拟地址空间的顶端;用户页表也有这个映射。Xv6在内核页表和每个用户页表中的同一个虚拟地址上映射了trampoline页
  • 内核栈页。每个进程都有自己的内核栈,内核栈被映射到高地址处,所以 xv6 可以在它后面留下一个未映射的守护页。守护页的 PTE 是无效的(不设置 PTE_V 位),这样如果内核栈溢出,很可能会引起异常,内核会报错。如果没有防护页,栈溢出时会覆盖其他内核内存,导致不正确的操作。

内核通过高地址映射使用它的栈空间,栈空间也可以通过直接映射的地址被内核访问。

每个用户态进程都有一个单独的页表,当 xv6 在进程间切换时,也会改变页表

image-20230222143445243

​ 用户态进程地址空间

为了检测用户栈溢出分配的栈内存,xv6 会在 stack 的下方放置一个无效的保护页。如果用户栈溢出,而进程试图使用栈下面的地址,硬件会因为该映射无效而产生一个缺页异常。

用户页表并不映射内核, 因为RISC-V硬件在trap过程中不切换页表,所以用户页表必须包含uservec的映射,即stvec指向的trap处理程序地址。uservec必须切换satp,使其指向内核页表;为了在切换后继续执行指令,uservec必须被映射到内核页表与用户页表相同的地址。

调度

轮询调度

xv6周期性地强制切换,以应对长时间不进行sleep操作的计算进程, 用定时器中断来驱动上下文切换

sleepwakeup允许一个进程放弃CPU,并睡眠等待某一事件,并允许另一个进程将睡眠的进程唤醒

优点

文件结构简单, 资料丰富, 编出的文件小, 不依赖加载器, 支持比较常用的posix api.

qemu 启动虚拟机时需要指定Machine 即 -M 参数指定对应的机型.
从我们之前分析的demo看, 默认以virt 启动, 因riscv vcpu 只能运行在S/U mode 下, 所以不需要关注opensbi的部分, 只需要关注u-boot 和 kernel的部分.
这里只说kernel的部分, kernel 在编译时也要按对应的机型编译, 如virt. 还有dtb的部分, kernel 和 qemu 中也是对应的.

新增Machine 只是对于没有开发板时的妥协,便于开发人员在没有开发板时有一个板子的虚拟环境进行测试验证, 所以可以看到该新增Machine的模拟硬件同开发板基本上是一致的;
对于虚拟化来说只有virt就可以了, virt上的feature也是最全的, 最适合跑guest os, 没有必要用其他Machine 跑guest os.

dtb 信息

在qemu 中一般会以代码的形式写dtb的相关属性, 可以用 qemu 导出相应机型的dtb信息

1
2
3
4
qemu-system-riscv64 -machine virt,dumpdtb=virt.dtb -smp 1 -m 2G -nographic
dtc virt.dtb > virt.dts
qemu-system-riscv64 -machine sifive_u,dumpdtb=sifive_u.dtb -smp 2 -m 2G -nographic
dtc sifive_u.dtb > sifive_u.dts

为了宏观的观察新增一个Machine 需要添加哪些部分, 可以先对比下 virt.dts 和 新增Machine 如sifive_u.dts的内容.

对于dtb 中描述的硬件信息, qemu都需要模拟出来对应的硬件.

qemu 中 dtb 信息的创建

qemu 对 libfdt 的接口做了二次封装,部分接口举例如下:

  1. create_device_tree:初始化一颗设备树
  2. qemu_fdt_setprop_cell:设置设备树的节点的属性,值为数字
  3. qemu_fdt_setprop_string:设置设备树的节点的属性,值为字符串
  4. qemu_fdt_add_subnode:添加一个子节点

sifive_u.c 中 create_fdt 函数就是使用上述的接口构建了整个dtb.

memory 信息

对于模拟的机型, 其中最重要的一块之一就是模拟的物理内存 GPA 的布局
如sifive_u的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static const MemMapEntry sifive_u_memmap[] = {
[SIFIVE_U_DEV_DEBUG] = { 0x0, 0x100 },
[SIFIVE_U_DEV_MROM] = { 0x1000, 0xf000 },
[SIFIVE_U_DEV_CLINT] = { 0x2000000, 0x10000 },
[SIFIVE_U_DEV_L2CC] = { 0x2010000, 0x1000 },
[SIFIVE_U_DEV_PDMA] = { 0x3000000, 0x100000 },
[SIFIVE_U_DEV_L2LIM] = { 0x8000000, 0x2000000 },
[SIFIVE_U_DEV_PLIC] = { 0xc000000, 0x4000000 },
[SIFIVE_U_DEV_PRCI] = { 0x10000000, 0x1000 },
[SIFIVE_U_DEV_UART0] = { 0x10010000, 0x1000 },
[SIFIVE_U_DEV_UART1] = { 0x10011000, 0x1000 },
[SIFIVE_U_DEV_PWM0] = { 0x10020000, 0x1000 },
[SIFIVE_U_DEV_PWM1] = { 0x10021000, 0x1000 },
[SIFIVE_U_DEV_QSPI0] = { 0x10040000, 0x1000 },
[SIFIVE_U_DEV_QSPI2] = { 0x10050000, 0x1000 },
[SIFIVE_U_DEV_GPIO] = { 0x10060000, 0x1000 },
[SIFIVE_U_DEV_OTP] = { 0x10070000, 0x1000 },
[SIFIVE_U_DEV_GEM] = { 0x10090000, 0x2000 },
[SIFIVE_U_DEV_GEM_MGMT] = { 0x100a0000, 0x1000 },
[SIFIVE_U_DEV_DMC] = { 0x100b0000, 0x10000 },
[SIFIVE_U_DEV_FLASH0] = { 0x20000000, 0x10000000 },
[SIFIVE_U_DEV_DRAM] = { 0x80000000, 0x0 },
};

从这个简表上大概也能看出涉及到的硬件资源有哪些, 如clint plic qspi pwm otp uart 等等, 而对应的这些硬件资源都需要qemu 进行模拟.
这些模拟的硬件资源的特性都应该跟硬件SPEC 上一致.

machine_init

对应machine的class_init, 注册 MachineClass -> init, 如sifive_u 为 sifive_u_machine_init
该函数由 /hw/core/machine.c#machine_run_board_init 调用
该函数涉及到的初始化的硬件资源有 cpu ddr/ram qspi gpio spi sdcard, sdcard跟SPI2 相连, dtb 信息也是在该函数中调用 create_fdt 创建的

以下均以sifive_u 的Machine展开介绍

cpu

child obj – TYPE_RISCV_U_SOC
qdev_realize(DEVICE(&s->soc), NULL, &error_fatal);

涉及到的重要函数有 cpu的 类初始化 实例初始化 具现化函数, 以sifive_u 举例
sifive_u_soc_class_init sifive_u_soc_instance_init sifive_u_soc_realize

sifive_u_soc_class_init的主要作用是指定具现化函数

instance_init

sifive_u_soc_instance_init

该函数除了定义cpu的一些属性外, 还引出了cpu关联的总线上的一些ip 硬件资源

1
2
3
4
5
6
7
8
9
10
11
12
static void sifive_u_soc_instance_init(Object *obj) {
... // 定义cpu的相关属性
object_initialize_child(obj, "prci", &s->prci, TYPE_SIFIVE_U_PRCI);
object_initialize_child(obj, "otp", &s->otp, TYPE_SIFIVE_U_OTP);
object_initialize_child(obj, "gem", &s->gem, TYPE_CADENCE_GEM);
object_initialize_child(obj, "gpio", &s->gpio, TYPE_SIFIVE_GPIO);
object_initialize_child(obj, "pdma", &s->dma, TYPE_SIFIVE_PDMA);
object_initialize_child(obj, "spi0", &s->spi0, TYPE_SIFIVE_SPI);
object_initialize_child(obj, "spi2", &s->spi2, TYPE_SIFIVE_SPI);
object_initialize_child(obj, "pwm0", &s->pwm[0], TYPE_SIFIVE_PWM);
object_initialize_child(obj, "pwm1", &s->pwm[1], TYPE_SIFIVE_PWM);
}

这些硬件资源每一个都需要展开, 需要qemu 模拟对应的特性.
这里抽一个简单的来看, 以otp 为例
最重要的函数为其具现化函数 sifive_u_otp_realize

对应总线上的ip, 最关键的是其 mmio的注册添加到io的memory region上, 当guest os 访问对应的物理地址时, 需要发生陷入, 最终陷入到qemu 中为该ip注册的mmio MemoryRegion上.

此处, otp相关的mmio MemoryRegion的注册过程如下, 对应的陷入的读写函数为 sifive_u_otp_read sifive_u_otp_write

1
2
3
4
5
6
7
8
9
10
11
static const MemoryRegionOps sifive_u_otp_ops = {
.read = sifive_u_otp_read,
.write = sifive_u_otp_write,
.endianness = DEVICE_NATIVE_ENDIAN,
.valid = {
.min_access_size = 4,
.max_access_size = 4
}
};
memory_region_init_io(&s->mmio, OBJECT(dev), &sifive_u_otp_ops, s,
TYPE_SIFIVE_U_OTP, SIFIVE_U_OTP_REG_SIZE);

在具现化函数中负责为该ip 进行具现化
sysbus_mmio_map(SYS_BUS_DEVICE(&s->otp), 0, memmap[SIFIVE_U_DEV_OTP].base);
其中 SIFIVE_U_DEV_OTP 的GPA 区域, 同硬件SPEC 上OTP的mmio 一致.
[SIFIVE_U_DEV_OTP] = { 0x10070000, 0x1000 },

realize

对应 sifive_u 的 具现化函数
sifive_u_soc_realize

包含 dtb中cpu的设置
这里设置了 u_cpus, 对应fu540, e_cpus 对应e24 单元
调用 sysbus_realize 进行platform 总线的设置
创建 “riscv.sifive.u.l2lim” 内存
plic 初始化
uart 初始化
clint 初始化
clint software interrupt 初始化
cline timer interrupt 初始化
各外设和ip的 mmio 设置

l1/l2 cache 结构

image-20240416112815996

cache控制器是如何判断数据是否在cache中命中呢?所以cache肯定是只能缓存主存中极小一部分数据。我们如何根据地址在有限大小的cache中查找数据呢?现在硬件采取的做法是对地址进行散列(可以理解成地址取模操作)

image-20240416112819790

我们一共有8行cache line,cache line大小是8 Bytes。所以我们可以利用地址低3 bits(如上图地址蓝色部分)用来寻址8 bytes中某一字节,我们称这部分bit组合为offset。
同理,8行cache line,为了覆盖所有行。我们需要3 bits(如上图地址黄色部分)查找某一行,这部分地址部分称之为index

tag array和data array一一对应。每一个cache line都对应唯一一个tag,tag中保存的是整个地址位宽去掉index和offset使用的bit剩余部分(如上图地址绿色部分)。tag、index和offset三者组合就可以唯一确定一个地址了。因此,当我们根据地址中index位找到cache line后,取出当前cache line对应的tag,然后和地址中的tag进行比较,如果相等,这说明cache命中。如果不相等,说明当前cache line存储的是其他地址的数据,这就是cache缺失。

Directory Mapped

image-20240416112823674

0x00、0x40 地址中index部分是一样的。因此,这2个地址对应的cache line是同一个。所以,当我们访问0x00地址时,cache会缺失
, 然后数据会从主存中加载到cache中第0行cache line;
当我们访问0x40地址时,依然索引到cache中第0行cache line,由于此时cache line中存储的是地址0x00地址对应的数据,所以此时依然会cache缺失。然后从主存中加载0x40地址数据到第一行cache line中

访问0x40地址时,就会把0x00地址缓存的数据替换。这种现象叫做cache颠簸(cache thrashing)

四路组相联

image-20240416112827437

降低了cache颠簸的频率

VIPT

我们可以使用虚拟地址对应的index位查找cache,与此同时(硬件上同时进行)将虚拟地址发到MMU转换成物理地址。当MMU转换完成,同时cache控制器也查找完成,此时比较cacheline对应的tag和物理地址tag域,以此判断是否命中cache。我们称这种高速缓存为VIPT(Virtually Indexed Physically Tagged)

image-20240416112831504

write back

当CPU执行store指令并在cache命中时,我们只更新cache中的数据。并且每个cache line中会有一个bit位记录数据是否被修改过,称之为dirty bit (D)
主存中的数据可能是未修改的数据,而修改的数据躺在cache中。cache和主存的数据可能不一致

self-modify l1 icache

self-modify

image-20240416112835829

cache coherence

缓存一致性协议 MESI

M : modified
E : exclusive
S : shared
I : invalid

协议在每一个 cache line 中维护一个两位的状态 “tag” ,这个 “tag” 在 cache line 的物理地址或者数据后

image-20240416112839686

MESI 演示

image-20240416112847184

image-20240416112851863

Store Buffer / Invalidate Queue

MESI解决了缓存一致性问题,但是它有一个性能弱点:
处理器执行写操作时,必须等待其他处理器将其高速缓存中的相应副本数据删除, 并接收到这些处理器所回复的Invalidate Acknowledge/Read Response消息之后才能将数据写入高速缓存。

image-20240416112855625

为了规避和减少这种等待造成的写操作的延迟(Latency), 引入了Store Buffer和Invalidate Queue。

Store Buffer是处理器内部的一个容量比高速缓存器还小的私有高速存储部件,每个处理器都有其写存储器,并且一个处理器无法读取另外一个处理器上的Store Buffer中的内容。

image-20240416112858588

处理器可以并不等待其他处理器返回Invalidate Acknowledge/Read Response消息而是继续执行其他指令。
当一个处理器接收到其他处理器所回复的针对同一个缓存行的所有Invalidate Acknowledge消息的时候,该处理器会将Store Buffer中针对相应地址的写操作的结果写入相应的缓存行,此时写操作对于其他处理器来说才算完成。

有了Store Buffer后,一个处理器在更新了一个变量之后,马上又读取了该变量的值,但是由于该处理器先前对该变量的更新结果可能仍然还停留在Store Buffer中,因此该变量相应的内存地址所对应的缓存行中仍存储着该变量的旧值。
因此处理器在执行读操作的时候会根据内存地址查询Store Buffer。

  • 如果Store Buffer存在该数据,那么会直接将该数据作为结果返回。
  • 如果不存在,处理器会从高速缓存中读取数据。

这种处理器直接从Store Buffer中读取数据来实现内存读操作的技术被称为存储转发(Store Buffer)

Invalidate Queue

引入Invalidate Queue之后,处理器在接收到Invalidate消息之后并不删除消息中指定地址的副本数据,而是将消息存入 Invalidate Queue之后就回复Invalidate Acknowledge消息,从而减少了执行写操作的处理器所需的等待问题。

image-20240416112903648

可见性

可见性即一个线程对共享变量值的修改,能够及时被其他线程看到。 但是由于Store Buffer和Invalidate Queue的存在,使得数据可能不被其他线程及时看到。

  1. 现代处理器在一些特定条件下(比如Store Buffer满,I/O指令被执行)会将Store Buffer中的内容写入高速缓存,但是这种写入并不一定是及时的,也就是说Store Buffer中的数据可能并没有刷新到高速缓存。
  2. 处理器在执行内存读取操作的时候如果没有根据Invalidate Queue中的内容将该处理器上的高速缓存中的相关副本数据删除,那么也可能导致该处理器读到的数据是过时的数据。

为了解决上面的两个问题,首先要使写线程对共享变量所做的更新能够及时到达高速缓存,从而使该更新对其他处理器是同步的;
其次,读线程所在的处理器要将其Invalidate Queue中的内容进行处理,保证读线程读到的数据是新的
。而底层系统会借助一类被称为内存屏障的特殊指令。

  • Store Barrier:可以使执行该指令的处理器冲刷其Store Buffer中的数据到缓存,从而保证某线程对共享变量所做的更新对读线程是可见的。
  • Load Barrier:会根据Invalidate Queue中内容所指定的内存地址,将处理器上高速缓存中的状态标记为I,从而使该处理器后续执行这些地址的读操作时必须发送Read消息,从而保证了处理器读到的数据是新的。

CM - cluster 内部

Setting the Cache Coherency Attributes for Default Memory Transfers 405
CCA_Override_Value 覆盖默认的L2 配置.

cluster 间同步

image-20240416112907383

无卡启动

配置tftpd

1
2
3
4
5
6
7
8
9
10
sudo apt install tftpd-hpa
sudo vi /etc/default/tftpd-hpa #编辑 /etc/default/tftpd-hpa
# /etc/default/tftpd-hpa

TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/srv/tftp"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="-s"

sudo chmod 777 -R /srv/tftp

配置完后, tftp 使用的目录为 /srv/tftp

将编出的 work/image.fit 拷贝到该文件夹中

本机测试

1
2
tftp localhost
> get image.fit

无错误代表没问题

开发板 u-boot tftp 下载

1
StarFive # setenv ipaddr 192.168.xx.xx;setenv serverip 192.168.xx.xx

serverip 为 tftpd 即刚才配置好 tftpd 的ip
ipaddr 为 开发版的 ip

开发板 u-boot 配置环境变量

  1. set enviroment parameter:

    1
    setenv bootfile vmlinuz; setenv fileaddr a0000000; setenv fdtcontroladdr 0xffffffffffffffff;
  2. upload image file to ddr:

    1
    tftpboot ${fileaddr} ${serverip}:image.fit;

    如果下载不了, 执行下 run bootcmd_dhcp 再执行下 tftpboot ${fileaddr} ${serverip}:image.fit

  3. load and excute:

    1
    bootm start ${fileaddr};bootm loados ${fileaddr};run chipa_set_linux;booti 0x40200000 0x46100000:${filesize} 0x46000000
  4. login

    1
    2
    buildroot login:root
    Password: starfive

Initialize SPI flash:

1
StarFive # sf probe

Update SPL binary

1
2
StarFive # tftpboot 0xa0000000 ${serverip}:u-boot-spl.bin.normal.out
StarFive # sf update 0xa0000000 0x0 $filesize

Update U-Boot binary

1
2
StarFive # tftpboot 0xa0000000 ${serverip}:visionfive2_fw_payload.img
StarFive # sf update 0xa0000000 0x100000 $filesize

Hypervisor Status Register (hstatus)

When VTSR=1, an attempt in VS-mode to execute SRET raises a virtual instruction exception.
When VTW=1 (and assuming mstatus.TW=0), an attempt in VS-mode to execute WFI raises a virtual instruction exception if the WFI does not complete within an implementation-specific, bounded time limit.
When VTVM=1, an attempt in VS-mode to execute SFENCE.VMA or to access CSR satp raises a virtual instruction exception.

The VGEIN (Virtual Guest External Interrupt Number) field selects a guest external interrupt source for VS-level external interrupts. VGEIN is a WLRL field that must be able to hold values between zero and the maximum guest external interrupt number (known as GEILEN), inclusive.
When VGEIN=0, no guest external interrupt source is selected for VS-level external interrupts. GEILEN may be zero, in which case VGEIN may be hardwired to zero.
Guest external interrupts are explained in Section 1.2.4, and the use of VGEIN is covered further in Section 1.2.3.

Field HU (Hypervisor User mode) controls whether the virtual-machine load/store instructions, HLV, HLVX, and HSV, can be used also in U-mode. When HU=1, these instructions can be executed in U-mode the same as in HS-mode. When HU=0, all hypervisor instructions cause an illegal instruction trap in U-mode.

hip & hie

Registers hip and hie are HSXLEN-bit read/write registers that supplement HS-level’s sip and sie respectively. The hip register indicates pending VS-level and hypervisor-specific interrupts, while hie contains enable bits for the same interrupts. As with sip and sie, an interrupt i will be taken in HS-mode if bit i is set in both hip and hie, and if supervisor-level interrupts are globally enabled.

Bits hip.SGEIP and hie.SGEIE are the interrupt-pending and interrupt-enable bits for guest external interrupts at supervisor level (HS-level).
SGEIP is read-only in hip, and is 1 if and only if the bitwise logical-AND of CSRs hgeip and hgeie is nonzero in any bit.

Bits hip.VSEIP and hie.VSEIE are the interrupt-pending and interrupt-enable bits for VS-level external interrupts. VSEIP is read-only in hip, and is the logical-OR of these interrupt sources:

  • bit VSEIP of hvip;
  • bit of hgeip selected by hstatus.VGEIN
  • any other platform-specific external interrupt signal directed to VS-level.

Bits hip.VSTIP and hie.VSTIE are the interrupt-pending and interrupt-enable bits for VS-level timer interrupts. VSTIP is read-only in hip, and is the logical-OR of:

  • hvip.VSTIP
  • any other platform-specific timer interrupt signal directed to VS-level.

Bits hip.VSSIP and hie.VSSIE are the interrupt-pending and interrupt-enable bits for VS-level software interrupts. VSSIP in hip is an alias (writable) of the same bit in hvip.

Hypervisor Guest External Interrupt Registers (hgeip and hgeie)

hgeip indicates pending guest external interrupts for this hart.
hgeie contains enable bits for the guest external interrupts at this hart.
Guest external interrupts represent interrupts directed to individual virtual machines at VS-level.
只有在设备直通场景下使用

interrupts from the device are intended for a specific virtual machine. Each bit of hgeip summarizes all pending interrupts directed to one virtual hart, as collected and reported by an interrupt controller. To distinguish specific pending interrupts from multiple devices, software must query the interrupt controller.

Support for guest external interrupts requires an interrupt controller that can collect virtual-machine-directed interrupts separately from other interrupts.

中断控制器需要收集virtual guest external interrupt, 和 物理cpu的external interrupt 区分开, guest 只能接收virtual external guest interrupt

The number of bits implemented in hgeip and hgeie for guest external interrupts is GEILEN
bits GEILEN:1 shall be writable in hgeie, and all other bit positions shall be hardwired to zeros in both hgeip and hgeie.

Guest external interrupt number i at one physical hart is typically expected not to be the same as guest external interrupt i at any other hart.
For any one physical hart, the maximum number of virtual harts that may directly receive guest external interrupts is limited by GEILEN.

A hypervisor is always free to emulate devices for any number of virtual harts without being limited by GEILEN. Only direct pass-through (direct assignment) of interrupts is affected by the GEILEN limit, and the limit is on the number of virtual harts receiving such interrupts, not the number of distinct interrupts received. The number of distinct interrupts a single virtual hart may receive is determined by the interrupt controller.

The enable bits in hgeie do not affect the VS-level external interrupt signal selected from hgeip by hstatus.VGEIN.

vsip & vsie

hideleg not zero(bit 10/6/2 不为0时)

  • vsip.SEIP and vsie.SEIE are aliases of hip.VSEIP and hie.VSEIE.
  • vsip.STIP and vsie.STIE are aliases of hip.VSTIP and hie.VSTIE.
  • vsip.SSIP and vsie.SSIE are aliases of hip.VSSIP and hie.VSSIE.

小结

hideleg 开启时(bit 10/6/2 不为0时)

  • vsip.SEIP = hip.VSEIP vsip.STIP = hip.VSTIP vsip.SSIP = hip.VSSIP
    • VSEIP is read-only in hip, and is the logical-OR of these interrupt sources:
      • bit VSEIP of hvip;
      • bit of hgeip selected by hstatus.VGEIN
      • any other platform-specific external interrupt signal directed to VS-level.
    • VSTIP is read-only in hip, and is the logical-OR of:
      • hvip.VSTIP
      • any other platform-specific timer interrupt signal directed to VS-level.
    • VSSIP in hip is an alias (writable) of the same bit in hvip.
      • 即 vsip.SSIP = hip.VSSIP = hvip.VSSIP
  • vsie.SEIE = hie.VSEIE vsie.STIE = hie.VSTIE vsie.SSIE = hie.SSIE

真正影响guest os的是vsip , guest os 运行时处于V=1 mode, vsip被替换为sip,

  • vsip software 中断来自于hvip (来自于HS hypervisor), 或者guest os 自己.
  • vsip timer 中断来自于hvip (来自于 HS hypervisor), 或guest os 自己, 或 platform-specific timer interrupt signal directed to VS-level
  • vsip guest external 中断来自于hvip (来自于HS hypervisor), 或hstatus.VGEIN -> hgeip 或 any other platform-specific external interrupt signal directed to VS-level

下面这两句应该怎么理解?

  • Bits hip.SGEIP and hie.SGEIE are the interrupt-pending and interrupt-enable bits for guest external interrupts at supervisor level (HS-level).
  • Bits hip.VSEIP and hie.VSEIE are the interrupt-pending and interrupt-enable bits for VS-level external interrupts.

直译:
SGEIP 和 SGEIE 是给HS 用的, 重点看 SGEIP, 这个是hgeip & hgeie and的结果, 代表的有guest external 中断待处理.
hip.VSEIP 和 hie.VSEIE 是给VS 用的, 这个好理解, 这两个直接反应到vsip vsie的相应bit上了.

结合这句:
if GEILEN is nonzero, bits GEILEN:1 shall be writable in hgeie
Only direct pass-through (direct assignment) of interrupts is affected by the GEILEN limit, and the limit is on the number of virtual harts receiving such interrupts, not the number of distinct interrupts received

背景:
不同的vcpu可以运行不同guest os.
一个物理cpu 有三套寄存器

  • v 开头的 vsip vsie 等
  • h 开头的 hvip hip hie hgeie hgeip 等
  • s 开头的 sip sie 等
    当V=0 => V=1 时, v开头的寄存器会替换成 s 开头的寄存器, 此时变成vcpu的执行环境
    V=1 时, 只能访问 s 开头的寄存器.

大概猜测, 在设备直通场景时, 0 < hstatus.VGEIN <= GEILEN(物理cpu上托管的vcpu的数量),

需要HS vmm 对 hgeie hstatus操作, 对应物理cpu上托管的vcpu
如当前物理cpu上托管了8个vcpu, 正在运行的是第2个vcpu

  • 对 hstatus.VGEIN 设置为2
  • 对 hgeie 的前8个bit 置1, 表示物理cpu 管了8个ready vcpu 的中断状态, 这8个vcpu都要处理guest external interrupt.

在前面前提下, 硬件需要将hip.VSEIP 与 hgeip的状态区分.
前面hip.VSEIP 的来源处说的比较模糊: “bit of hgeip selected by hstatus.VGEIN

不太好理解, 大概猜测:

在设备直通场景(设置了hstatus.VGEIN时), 中断控制器需要判断给哪个vcpu, 导致的直接结果就是要设置hgeip 的哪个bit, 同时硬件应该将 hip.VSEIP 置为hgeip 与 hstatus.VGEIN 逻辑与 的结果. 而hip.SGEIP 置为 hgeip & hgeie 逻辑与的结果.

假如中断控制器要发给第三个vcpu, 就需要将hgeip 的第三个bit 置1

  • 情景1 : 假设物理cpu的状态 V=1 mode 正在运行第二个vcpu, hstatus.VGEIN = 2:
    此时因为正在运行的是第二个vcpu, hstatus.VGEIN=2, 则hip.VSEIP = 0, 而 hip.SGEIP 为 1, 因为vsip.SEIP->sip.SEIP = hip.VSEIP, 此时vsip.SEIP 没有置位(此时假设只有外部中断, SSIP STIP 都是0), 此时硬件根据vsip penging为无信号, 而hip.SGEIP 有信号, 不能将中断委托给vcpu, 而应将中断给到 host os HS-mode的vmm.

    从vcpu陷入到hypervisor vmm 后, vmm 需要check hip.SGEIP & hie SGEIE(或hip&hie), 有待处理的虚拟外部中断, 进而查hgeip, 查到是第三个vcpu的, 则切换到第三个vcpu 运行, 切换前将hstatus.VGEIN 设置为3. 此时vsip.SEIP = hip.VSEIP 会被置1(hgeip 逻辑与 hstatus.VGEIN) , 第三个vcpu 陷入V=1 mode, 处理虚拟外部中断. 如guest os kernel 将sie.SEIE 置过位, 则guest os 会处理external 中断(10号guest external中断会转换成9 号external 中断), guest os 需要查询中断控制器, 判断外部中断是谁的, 该由谁的中断处理函数处理. 处理完后将中断控制器的pending 清0(该操作导致中断控制器把hgeip清0), 返回到 hypervisor vmm 后, vsip.SEIP = hip.VSEIP 也会因hgeip 而清0.

  • 情景2: 假设物理cpu的状态 V=0 mode, 处于host下
    中断由host os接收
    hypervisor vmm 需要check hip.SGEIP & hie SGEIE (或hip&hie), 有待处理的虚拟外部中断, 进而查hgeip, 查到是第三个vcpu的, 则切换到第三个vcpu 运行, 切换前将hstatus.VGEIN 设置为3. 此时vsip.SEIP = hip.VSEIP 会被置1(hgeip 逻辑与 hstatus.VGEIN) , 第三个vcpu 陷入V=1 mode, 处理虚拟外部中断. 如guest os kernel 将sie.SEIE 置过位, 则guest os 会处理external 中断(10号guest external中断会转换成9 号external 中断), guest os 需要查询中断控制器, 判断外部中断是谁的, 该由谁的中断处理函数处理. 处理完后将中断控制器的pending 清0(该操作导致中断控制器把hgeip清0), 返回到 hypervisor vmm 后, vsip.SEIP = hip.VSEIP 也会因hgeip 而清0.

  • 情景3: 假设物理cpu的状态 V=1 mode 正在运行第三个vcpu, hstatus.VGEIN = 3:
    hgeip 逻辑与 hstatus.VGEIN 不为0, hip.VSEIP 置1.
    因为vsip.SEIP->sip.SEIP = hip.VSEIP, 此时vsip.SEIP 置位, 此时硬件根据vsip penging有信号, hip.SGEIP 有信号, 应将中断给到 vcpu guest os.
    vcpu处理虚拟外部中断. 如guest os kernel 将sie.SEIE 置过位, 则guest os 会处理external 中断(10号guest external中断会转换成9 号external 中断), guest os 需要查询中断控制器, 判断外部中断是谁的, 该由谁的中断处理函数处理. 处理完后将中断控制器的pending 清0(该操作导致中断控制器把hgeip清0), 返回到 hypervisor vmm 后, vsip.SEIP = hip.VSEIP 也会因hgeip 而清0.

备注

hgeip read-only csr, 由硬件操作
hgeie rw csr, 由软件操作
hip.SGEIP read-only bit, 由硬件操作
hip.VSEIP read-only bit, 由硬件操作
sip.SEIP read-only bit, 由硬件操作, typically through a platform-specific interrupt controller.

riscv 直通场景 AIA IMSIC

riscv 直通场景

IMSIC

hvictl

Hypervisor Virtual Interrupt Control

  • 触发VS major 中断(hvien 和 hvip 不支持的)
  • 支持配置VS-mode 中断优先级( hviprio1/hviprio2 之外的)
  • 模拟一个外部中断控制器, 没有使用IMSIC的 guest interrupt file. 同时支持为外部中断和virtual hart的major interrupts 配置中断优先级

hvien

guest 不需要配置major 中断的优先级, 除了硬件自定义实现的那些.
hvien 保留了低12位(ro 符合SPEC规范的), hvien 和 hvip 配合来表明注入对应的中断, 除了SPEC 规范的低12位外, 低12位由SPEC 定义, 如VSEIP/VSTIP/VSSIP. 而13:63 位用来实现硬件自定义实现的中断.

hideleg 开启后, vsip 的位等同于 sip的位

  • hideleg 的某 bit位为0时, 而hvien的对应bit位为1, 此时vsip 的该bit位等同于hvip的该bit位
  • hideleg 的某 bit位为0时, 而hvien的对应bit位为0, 此时vsip 的该bit位是ro的,

参考文档
Documentation/devicetree/bindings/interrupt-controller/riscv,imsics.yaml

dts 信息

ex:
提供了两个interrupt file M-mode 和 S-mode的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
imsic_mlevel: interrupt-controller@24000000 {
compatible = "riscv,qemu-imsics", "riscv,imsics";
interrupts-extended = <&cpu1_intc 11>,
<&cpu2_intc 11>,
<&cpu3_intc 11>,
<&cpu4_intc 11>;
reg = <0x28000000 0x4000>;
interrupt-controller;
#interrupt-cells = <0>;
msi-controller;
riscv,num-ids = <127>;
};

imsic_slevel: interrupt-controller@28000000 {
compatible = "riscv,qemu-imsics", "riscv,imsics";
interrupts-extended = <&cpu1_intc 9>,
<&cpu2_intc 9>,
<&cpu3_intc 9>,
<&cpu4_intc 9>;
reg = <0x28000000 0x2000>, /* Group0 IMSICs */
<0x29000000 0x2000>; /* Group1 IMSICs */
interrupt-controller;
#interrupt-cells = <0>;
msi-controller;
riscv,num-ids = <127>;
riscv,group-index-bits = <1>;
riscv,group-index-shift = <24>;
};

下面对上述dts的相关属性做一个大概的介绍

  1. interrupts-extended
    对应cpu1-4, 每个cpu都有这两个interrupt file M-mode和S-mode的
    M的对应中断位为11, 代表 Machine external interrupt
    S的对应中断位为9, 代表 Supervisor external interrupt
  2. riscv,num-ids
    Number of interrupt identities supported by IMSIC interrupt file.
    外部中断数量, 反映到eip和eie中, 最小63 最大 2047
  3. riscv,group-index-bits
    Number of group index bits in the MSI target address. When not specified it is assumed to be 0.
    总的group index bits
  4. riscv,group-index-shift
    The least significant bit position of the group index bits in the MSI target address. When not specified it is assumed to be 24.
  5. reg
    Base address of each IMSIC group.
    关于MSI target address:
    从第24 bit 开始, 上述group-index-bits 表示在Group Index中总共有几个bit 被使用
    riscv,group-index-shift 表示Group Index中当前interrupt file 占位开始的那个bit位, 如其占了两个bit位 26-27, shift表示最开始的bit位 26.
1
2
3
4
5
XLEN-1           >=24                                 12    0
| | | |
-------------------------------------------------------------
|xxxxxx|Group Index|xxxxxxxxxxx|HART Index|Guest Index| 0 |
-------------------------------------------------------------

guest os相关

guest external interrupt 相关的dts信息

  1. riscv,num-guest-ids
    Number of interrupt identities are supported by IMSIC guest interrupt
    file. When not specified it is assumed to be same as specified by the
    riscv,num-ids property.
    guest external 硬件中断号 最小63 最大2047
  2. riscv,hart-index-bits
    Number of HART index bits in the MSI target address. When not
    specified it is estimated based on the interrupts-extended property.
    MSI target address 中 Hart Index 总共使用了几个bit位.
    最小0 最大 15
  3. riscv,guest-index-bits
    Number of HART index bits in the MSI target address. When not
    specified it is estimated based on the interrupts-extended property.
    MSI target address 中 Guest Index 总共使用了几个bit位.
    最小0 最大 7

下面开始大概分析 imsic 的代码

代码部分

imsic_init

dts 相关封装的接口

1
2
3
4
5
6
7
8
9
struct imsic_fwnode_ops ops = {
.nr_parent_irq = imsic_dt_nr_parent_irq,
.parent_hartid = imsic_dt_parent_hartid,
.nr_mmio = imsic_dt_nr_mmio,
.mmio_to_resource = imsic_mmio_to_resource,
.mmio_map = imsic_dt_mmio_map,
.read_u32 = imsic_dt_read_u32,
.read_bool = imsic_dt_read_bool,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
-+ imsic_init(struct imsic_fwnode_ops *fwops, struct fwnode_handle *fwnode, void *fwopaque)
\ - check riscv_isa_extension_available(NULL, SxAIA) "RISCV_ISA_EXT_SxAIA -> RISCV_ISA_EXT_SSAIA ?"
| - priv = kzalloc(sizeof(*priv), GFP_KERNEL);
| - global = &priv->global;
| -+ nr_parent_irqs = fwops->nr_parent_irq(fwnode, fwopaque);
\ -+ imsic_dt_nr_parent_irq(fwnode, fwopaque)
\ - of_irq_count(to_of_node(fwnode)); "一般会读到 dts的 interrupt-extended 属性中, 代表cpu的中断域
如前面dts中一共有S-mode 和 M-mode的, 每个有1-4 cpu的中断域, 这里的值 nr_parent_irqs 即为8"
| - rc = fwops->read_u32(fwnode, fwopaque, "riscv,guest-index-bits", &global->guest_index_bits);
"调用 imsic_dt_read_u32 读 riscv,guest-index-bits 读出 Msi target address 中 Guest Index 总共使用了几个bit位,
结果放入 priv->global->guest_index_bits"
| - rc = fwops->read_u32(fwnode, fwopaque, "riscv,hart-index-bits", &global->hart_index_bits);
"读取 dts riscv,hart-index-bits , 读出 Msi target address 中 Hart Index 总共使用了几个bit位, 结果放入priv->global->hart_index_bits"
| - rc = fwops->read_u32(fwnode, fwopaque, "riscv,group-index-bits", &global->group_index_bits);
| - rc = fwops->read_u32(fwnode, fwopaque, "riscv,group-index-shift", &global->group_index_shift);
| - rc = fwops->read_u32(fwnode, fwopaque, "riscv,num-ids", &global->nr_ids); "多少个hwirq"
| - fwops->read_u32(fwnode, fwopaque, "riscv,num-guest-ids", &global->nr_guest_ids) "多少个guest 可以使用的hwirq"
| -+ fwops->mmio_to_resource(fwnode, fwopaque, 0, &res);
\ -+ imsic_mmio_to_resource(fwnode, fwopaque, 0, &res); "Compute base address"
\ - of_address_to_resource(to_of_node(fwnode), index, res); "在设备树中找到第一个"reg",并将解析到的信息填充在"res"结构体里"
| - global->base_addr = res.start;
| - global->base_addr &= ~(BIT(global->guest_index_bits + global->hart_index_bits + IMSIC_MMIO_PAGE_SHIFT) - 1);
"Msi target address 中 屏蔽低 "guest bit + hart bit +12" 位"
| - global->base_addr &= ~((BIT(global->group_index_bits) - 1) << global->group_index_shift);
"Msi target address 中 屏蔽 其他group 的bit 位, 剩下的才是本group 使用的bit位"
"这个地方结合前面的dts信息, 这个做完之后应该就是0x28000000"
| -+ priv->num_mmios = fwops->nr_mmio(fwnode, fwopaque);
\ -+ imsic_dt_nr_mmio(fwnode, fwopaque) "Find number of MMIO register sets"
\ -+ while (!of_address_to_resource(to_of_node(fwnode), ret, &res)) "依次查剩下的reg , 结果放到 res中"
\ - ret++
| return ret "即dts中 总共几个 reg 信息"
| - priv->mmios = kcalloc(priv->num_mmios, sizeof(*mmio), GFP_KERNEL); "分配mmios 数组"
| -+ for i in priv->num_mmios "i从0 开始"
\ - mmio = &priv->mmios[i];
| - rc = fwops->mmio_to_resource(fwnode, fwopaque, i, &res); "从0 开始, 因此第一个仍是 M-mode的cpu的中断域"
| - mmio->pa = res.start;
| - mmio->size = res.end - res.start + 1; "这个是dts中 reg 的第二个字段 决定了size"
| - mmio->va = fwops->mmio_map(fwnode, fwopaque, i); "为pa 建立页表映射, 返回va"
| -+ imsic_ids_init(priv) "为hwirq 初始化 bitmap 位图"
\ - priv->ids_used_bimap = kcalloc(BITS_TO_LONGS(global->nr_ids + 1), sizeof(unsigned long), GFP_KERNEL);
| - priv->ids_target_cpu = kcalloc(global->nr_ids + 1, sizeof(unsigned int), GFP_KERNEL);
| -+ for (i = 0; i <= global->nr_ids; i++)
\ - priv->ids_target_cpu[i] = UINT_MAX; \
"这个ids_target_cpu数组维护了每个hwirq 同 cpuid 的映射关系, 即该hwirq 会发送到哪个hart上, 设置hwirq的亲和性时会用到"
| - bitmap_set(priv->ids_used_bimap, 0, 1); "清空hwirq的 used bitmap"
| -+ for (i = 0; i < nr_parent_irqs; i++)
\ - rc = fwops->parent_hartid(fwnode, fwopaque, i, &hartid); "找到 对应的hartid, 这里举例的一个8个, 每个hart 有M/S 两个"
| - cpu = riscv_hartid_to_cpuid(hartid); "hartid -> cpuid"
| - reloff = i * BIT(global->guest_index_bits) * IMSIC_MMIO_PAGE_SZ; "相对于 mmio base_addr 的偏移"
"这里将guest_index_bits 计入了, 代表每个hart的mmio 需要包含 guest interrupt file的地址空间"
| - handler = per_cpu_ptr(&imsic_handlers, cpu); "每个cpu 一个 handler 结构"
| - handler->local.msi_pa = mmio->pa + reloff; "msi_pa 加上该hart mmio的 相对于 mmio_base_addr 的偏移量"
| - handler->local.msi_va = mmio->va + reloff; "msi_va 同上"
| - domain = irq_find_matching_fwnode(riscv_get_intc_hwnode(), DOMAIN_BUS_ANY); "cpu-intc"
| - imsic_parent_irq = irq_create_mapping(domain, RV_IRQ_EXT); "IRQ_S_EXT 9 为S-mode 外部中断, 建立hwirq 同 linux irq的映射关系"
| - irq_set_chained_handler(imsic_parent_irq, imsic_handle_irq);
"cpu 来了 9号中断 S external interrupt 后, 由 imsic_handle_irq 处理函数进行处理该中断"
| - imsic_ipi_domain_init(priv); "ipi_domain"
| - imsic_irq_domains_init(priv, fwnode); "/* Initialize IRQ and MSI domains */"
| -+ cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "irqchip/riscv/imsic:starting", imsic_starting_cpu, NULL);
"注册 cpu startup 为 imsic_starting_cpu(), cpu online 时回调 startup "
\ -+ imsic_starting_cpu(cpu)
\ - enable_percpu_irq(imsic_parent_irq, irq_get_trigger_type(imsic_parent_irq));
"软件层 所有cpu 打开 映射了 hwirq为 IRQ_S_EXT的 linux irq 的中断"
| -+ imsic_ipi_enable(priv); ""
\ -+ __imsic_id_enable(priv->ipi_id); "imsci 寄存器打开 ipi 中断"
\ -+ __imsic_eix_update((priv->ipi_id), 1, false, true)
"让iselect 选择对应的 eip/eie 寄存器, 然后选择置1或置0 来开关中断/开关pending"
\ -+ imsic_csr_set(isel, ireg);
\ - csr_write(CSR_ISELECT, isel) "更新 siselct csr"
| - csr_set(CSR_IREG, ireg) "更新 sireg csr"

比较重要的数据:
global->base_addr
handler->local.msi_pa / handler->local.msi_va

imsic_handle_irq

S external interrupt 中断来了之后, 会由 imsic_handle_irq 函数进行处理, 简单看一下外部中断处理的流程

1
2
3
4
5
6
7
8
9
-+ imsic_handle_irq(struct irq_desc *desc)
\ - imsic_handler *handler = this_cpu_ptr(&imsic_handlers); "找到前面为每个hart 创建的handler "
| - irq_chip *chip = irq_desc_get_chip(desc); "这个芯片注册的irq的相关 ops"
| - chained_irq_enter(chip, desc); "在级联的中断处理函数中,调用`chained_irq_enter`进入中断级联处理"
| -+ while ((hwirq = csr_swap(CSR_TOPEI, 0)) "读 stopei csr, 该csr 为 imsic 专属, 其 27:16 代表hwirq"
"这个地方为什么用while, 猜测可能一次有多个外部中断, 每次对stopei 清0后, 中断控制器会为其设置次一个优先级的中断标识, stopei csr 是eipx & eiex 联合选择的结果, 从高优先级->低优先级 依次选择供给stopei 相应的中断标识"
\ - hwirq = hwirq >> TOPEI_ID_SHIFT;
| - generic_handle_domain_irq(priv->base_domain, hwirq);
"找到 外设驱动 request_irq 为该hwirq 注册的irq_handler 进行中断处理"

还是大体上是这个结构, 这个上面也有loop, 代表需要多次处理stopei, 从高优先级->低优先级依次处理完本次来的所有的S-mode的外部中断.

chip imsic 相关hook

1
2
3
4
5
6
7
8
9
10
11
static struct irq_chip imsic_irq_base_chip = {
.name = "RISC-V IMSIC-BASE",
.irq_mask = imsic_irq_mask,
.irq_unmask = imsic_irq_unmask,
#ifdef CONFIG_SMP
.irq_set_affinity = imsic_irq_set_affinity,
#endif
.irq_compose_msi_msg = imsic_irq_compose_msi_msg,
.flags = IRQCHIP_SKIP_SET_WAKE |
IRQCHIP_MASK_ON_SUSPEND,
};

imsic_irq_set_affinity

在外设驱动注册hwirq 中断时, 会走到 irq_set_affinity, 进而调用chip的 irq_set_affinity 钩子.
对于imsic来说, 调用 imsic_irq_set_affinity 函数设置cpu亲和性, 即该外部中断由哪个cpu进行处理
简单分析下这个函数

1
2
3
4
5
6
7
8
9
10
11
12
-+ imsic_irq_set_affinity(struct irq_data *d, const struct cpumask *mask_val, bool force)
"与cpuid 相关的在 mask_val 参数中"
\ -+ imsic_get_cpu(priv, mask_val, force, &target_cpu);
\ - cpumask_and(&amask, &priv->lmask, mask_val);
| -|+ if force
\ - cpu = cpumask_first(&amask); "直接从 cpu_mask mask_val中选出对应的cpu"
| -|+ if !force
\ - cpu = cpumask_any_and(&amask, cpu_online_mask); "优先从上线的cpu中再结合 cpu_mask 选出对应的cpu"

| - *out_target_cpu = cpu;
| -+ imsic_id_set_target(priv, d->hwirq, target_cpu); "? 并没有硬件寄存器相关的操作"
\ - priv->ids_target_cpu[id] = target_cpu; "? "

说明与hwirq 开启需要设置的csr 操作并未在这个函数中

  1. of_irq_get -> irq_create_of_mapping -> irq_domain_alloc_irqs -> irq_domain_alloc_irqs_hierarchy -> domain->ops->alloc(domain, irq_base, nr_irqs, arg)
  2. request_irq -> request_threaded_irq -> irq_startup -> __irq_startup -> irq_enable -> unmask_irq(desc) -> chip->irq_unmask(&desc->irq_data);

.alloc = imsic_irq_domain_alloc

1
2
3
4
static const struct irq_domain_ops imsic_base_domain_ops = {
.alloc = imsic_irq_domain_alloc,
.free = imsic_irq_domain_free,
};

相关堆栈

1
2
3
4
5
6
#1  0xffffffff8006a134 in irq_domain_alloc_irqs_hierarchy (arg=0xff2000000060b728, nr_irqs=1, irq_base=2, domain=0xff6000007fe0c800) at ../kernel/irq/irqdomain.c:1426
#2 __irq_domain_alloc_irqs (domain=domain@entry=0xff6000007fe0c800, irq_base=irq_base@entry=-1, nr_irqs=nr_irqs@entry=1, node=node@entry=-1, arg=arg@entry=0xff2000000060b728, realloc=realloc@entry=false, affinity=affinity@entry=0x0) at ../kernel/irq/irqdomain.c:1482
#3 0xffffffff8006a5d0 in irq_domain_alloc_irqs (arg=0xff2000000060b728, node=-1, nr_irqs=1, domain=0xff6000007fe0c800) at ../include/linux/irqdomain.h:516
#4 irq_create_fwspec_mapping (fwspec=fwspec@entry=0xff2000000060b728) at ../kernel/irq/irqdomain.c:825
#5 0xffffffff8006a6ee in irq_create_of_mapping (irq_data=irq_data@entry=0xff2000000060b7a8) at ../kernel/irq/irqdomain.c:858
#6 0xffffffff80515156 in of_irq_get (dev=dev@entry=0xff600000bfdf5200, index=index@entry=0) at ../drivers/of/irq.c:444

unmask_irq 相关堆栈

1
2
3
4
5
6
7
8
#0  plic_irq_unmask (d=0xff6000007fefec20) at ../drivers/irqchip/irq-sifive-plic.c:122
#1 0xffffffff80067d16 in unmask_irq (desc=<optimized out>) at ../kernel/irq/chip.c:435
#2 irq_enable (desc=desc@entry=0xff6000007fefec00) at ../kernel/irq/chip.c:342
#3 0xffffffff80067d72 in __irq_startup (desc=desc@entry=0xff6000007fefec00) at ../kernel/irq/chip.c:246
#4 0xffffffff80067e4e in irq_startup (desc=desc@entry=0xff6000007fefec00, resend=resend@entry=true, force=force@entry=false) at ../kernel/irq/chip.c:267
#5 0xffffffff80065a8a in __setup_irq (irq=irq@entry=1, desc=desc@entry=0xff6000007fefec00, new=new@entry=0xff6000007ff24f80) at ../kernel/irq/manage.c:1777
#6 0xffffffff80065d7c in request_threaded_irq (irq=<optimized out>, handler=handler@entry=0xffffffff80386ca6 <vp_interrupt>, thread_fn=thread_fn@entry=0x0, irqflags=irqflags@entry=128, devname=0xff60000080172858 "virtio2", dev_id=dev_id@entry=0xff6000008015b800) at ../kernel/irq/manage.c:2206
#7 0xffffffff80387540 in request_irq (dev=0xff6000008015b800, name=<optimized out>, flags=128, handler=<optimized out>, irq=<optimized out>) at ../include/linux/interrupt.h:168

imsic_irq_domain_alloc

1
2
3
4
5
6
7
8
9
-+ imsic_irq_domain_alloc(struct irq_domain *domain,
unsigned int virq,
unsigned int nr_irqs,
void *args)
\ - imsic_priv *priv = domain->host_data;
| - imsic_get_cpu(priv, &priv->lmask, false, &cpu); "获取"
| - imsic_cpu_page_phys(cpu, 0, &msi_addr);
| - hwirq = imsic_ids_alloc(priv, priv->global.nr_ids, get_count_order(nr_irqs));
| - imsic_id_set_target(priv, hwirq + i, cpu);

unmask_irq -> imsic_irq_unmask

1
2
3
4
5
6
7
8
9
10
-+ imsic_irq_unmask(struct irq_data *d)
\ -+ imsic_id_enable(irq_data_get_irq_chip_data(d), d->hwirq);
\ - bitmap_set(priv->ids_enabled_bimap, id, 1); "used bitmap 位图中 hwirq 的bit置1"
\ -+ __imsic_id_enable(priv->ipi_id); "imsci 寄存器打开 ipi 中断"
\ -+ __imsic_eix_update((priv->ipi_id), 1, false, true)
"让iselect 选择对应的 eip/eie 寄存器, 然后选择置1或置0 来开关中断/开关pending"
\ -+ imsic_csr_set(isel, ireg);
\ - csr_write(CSR_ISELECT, isel) "更新 siselct csr"
| - csr_set(CSR_IREG, ireg) "更新 sireg csr"

最终由unmask_irq -> imsic_irq_unmask 打开中断, 最后设置了该hart的 siselct 和 sireg csr, 将对应的eipx/eiex 置位.

小结与思考

从上面中断注册和中断处理过程中, 可以看到每个 hart 对应的MSI的mmio
对于S-mode来说, 外部中断设置的最终是
__imsic_id_enable(id)
该函数最终设置的是 eipx/eiex, 通过siselct 与 sireg 这两个csr 选择对应的 eipx/eiex, 再设置对应的值, 即可开关对应的pending/中断使能, csr 是hart 专属的, 每一个hart 都由一组csr.

imsic_irq_mask imsic_irq_unmask 最终关联 __imsic_id_enable __imsic_id_disable 这一组函数

irq_mask 中断屏蔽
irq_unmask 中断打开

但这其中并未发现直接的mmio 地址关联
上述中断开关最终都是操作的siselect/sireg 寄存器, 猜测最终都是设置的该hart的 S-mode interrupt file的mmio region.

与guest interrupt file 相关的设置在哪里呢, 怀疑的地方是msi相关的函数 imsic_irq_compose_msi_msg
该提交中看起来并没有涉及到 guest 相关的设置.
除了初始化函数中与guest_index 相关的内容外, 并没有其他的.

以 sifive fu740 为例

编译流程

编译opensbi

1
2
3
4
git clone https://github.com/riscv/opensbi.git
cd opensbi
make PLATFORM=generic
export OPENSBI=<path to opensbi/build/platform/generic/firmware/fw_dynamic.bin>

生成 fw_dynamic.bin
编译uboot 和 spl

1
2
3
cd <U-Boot-dir>
make sifive_unmatched_defconfig
make

生成 spl/u-boot-spl.binu-boot.itb 文件

烧写

1
2
3
4
5
6
sudo sgdisk -g --clear -a 1 \
--new=1:34:2081 --change-name=1:spl --typecode=1:5B193300-FC78-40CD-8002-E86C45580B47 \
--new=2:2082:10273 --change-name=2:uboot --typecode=2:2E54B353-1271-4842-806F-E436D6AF6985 \
--new=3:16384:282623 --change-name=3:boot --typecode=3:0x0700 \
--new=4:286720:13918207 --change-name=4:root --typecode=4:0x8300 \
/dev/sdX

sdX 表示通配, sd 卡插到电脑上, sd 卡的节点可能是 sdb sdc 等, 这里以 sdX 表示

1
2
3
4
5
6
7
8
9
10
sudo sgdisk -g --clear -a 1 \
--new=1:34:2081 --change-name=1:spl --typecode=1:5B193300-FC78-40CD-8002-E86C45580B47 \
--new=2:2082:10273 --change-name=2:uboot --typecode=2:2E54B353-1271-4842-806F-E436D6AF6985 \
--new=3:16384:282623 --change-name=3:boot --typecode=3:0x8300 \
/dev/nbd0
sudo dd if=<payload.bin> of=/dev/nbd0p2 seek=2082 "将u-boot.itb 拷贝到sd卡的第二个分区"
sudo mount /dev/nbd0p3 <mnt_dir>
cp Image dtb <mnt_dir>
sudo umount mnt_dir
sudo qemu-nbd --disconnect

上述步骤在sd卡上建了gpt 的四个分区, 分区的 start end sector offset 见命令, 同时指定了spl和u-boot.itb 所在分区的uuid

u-boot.itb 由 fw_dynamic.bin, u-boot-nodtb.binhifive-unmatched-a00.dtb 组成

1
2
3
4
5
6
sudo mkfs.vfat /dev/sdX3   "第三个分区格式化为 vfat 文件系统"
sudo mkfs.ext4 /dev/sdX4 "第四个分区格式化为 ext4 文件系统"
sudo mount /dev/sdX3 /media/sdX3 "第三个分区挂载到pc上"
sudo cp Image.gz hifive-unmatched-a00.dtb /media/sdX3/ "将kernel 镜像 dtb 文件拷贝到 sd卡的第三个分区下"
sudo dd if=spl/u-boot-spl.bin of=/dev/sdX seek=34 "将u-boot-spl.bin 拷贝到sd卡的第一个分区"
sudo dd if=u-boot.itb of=/dev/sdX seek=2082 "将u-boot.itb 拷贝到sd卡的第二个分区"

启动kernel

1
2
3
fatload mmc 0:3 ${kernel_addr_r} Image.gz                "kernel_addr_r 为 ddr上运行地址"
fatload mmc 0:3 ${fdt_addr_r} hifive-unmatched-a00.dtb "dtb 地址"
booti ${kernel_addr_r} - ${fdt_addr_r} "手动启动kernel"

spl代码分析

先分析入口汇编

spl 运行在M-mode下, 其bin文件被烧到了 sd卡的第一个分区中, 由bootrom 加载, u-boot-spl.bin 包含两个部分: u-boot-spl-nodtb.bin 和 u-boot-spl.dtb
u-boot-spl.dtb 中为spl的dtb 描述文件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
+ _start:
\ - mv tp, a0 "tp register save hartid"
| - la t0, trap_entry
| - csrw MODE_PREFIX(tvec), t0 "设置中断mtvec 入口为 trap_entry"
| - csrw MODE_PREFIX(ie), zero
| - li t0, MIE_MSIE
| - csrs   MODE_PREFIX(ie), t0 "初始化msie"
| -+ call_board_init_f "Set sp in internal/ex RAM to call board_init_f"
\ - li t1, CONFIG_SPL_STACK "设置初始栈基址"
| - li t0, -16
| - and   sp, t1, t0 "将堆栈16 bits对齐"
| -+ call_board_init_f_0 "从堆栈开始的地方,为u-boot的global data(struct global_data)分配空间"
\ -- mv a0, sp
| -+ jal board_init_f_alloc_reserve
\ - top -= CONFIG_VAL(SYS_MALLOC_F_LEN) "如定义了CONFIG_SYS_MALLOC_F_LEN,需预留出early malloc所需的空间"
| - slli t0, tp, CONFIG_STACK_SIZE_SHIFT "tp代表的是hartid, 每个hart 分配STACK_SIZE 空间"
| - sub sp, a0, t0 "sp = a0 - t0, 地址在初始栈基址上向上增长, a0 初始栈基址 top - malloc空间"
| - bnez   tp, secondary_hart_loop "其他hart 跳转到 secondary_hart_loop, hart 0 继续"
| -+ jal board_init_f_init_reserve "初始化uboot的global data, 置0"
\ - "如定义了SYS_MALLOC_F_LEN,则会初始化gd->malloc_base"
| - SREG tp, GD_BOOT_HART(gp) "/* save the boot hart id to global_data */"
| -+ wait_for_gd_init
\ - jal    icache_enable
| - jal    dcache_enable
| - mv a0, zero
| -+ jalr board_init_f
\ -+ spl_early_init()
\ -+ spl_common_init(true);
\ - fdtdec_setup() "扫描spl的dtb, 创建fdt blob"
\ - dm_init_and_scan(!CONFIG_IS_ENABLED(OF_PLATDATA)); "扫描注册了哪些driver, 绑定device"
| -+ riscv_cpu_setup(NULL, NULL);
\ -+ riscv_cpu_probe();
\ -+ cpu_probe_all(); "触发riscv cpu 驱动bind"
\ - riscv_cpu_bind() "主要工作是 riscv timer 相关初始化"
| - riscv_cpu_ops = {.get_desc  = riscv_cpu_get_desc, .get_info  = riscv_cpu_get_info,
.get_count = riscv_cpu_get_count,} "注册 riscv_cpu_ops"
| - check supports_extension('d') supports_extension('f') "通过前面注册的 riscv_cpu_get_desc 查询"
| - csr_write(CSR_MCOUNTEREN, GENMASK(2, 0)); "Enable perf counters for cycle, time,"
| -+ preloader_console_init();
\ - serial_init(); "初始化uart"
| -+ spl_board_init_f();
\ -+ spl_soc_init();
\ -+ uclass_get_device(UCLASS_RAM, 0, &dev); "ddr init"
\ - uclass_find_device(id, index, &dev);
| -+ uclass_get_device_tail(dev, ret, devp);
\ -+ device_probe(dev);
\ -+ sifive_ddr_probe(struct udevice *dev) "ddr probe"
\ - clk_get_by_index(dev, 0, &priv->ddr_clk); "从fdt 中获取"
| - dev_read_u32(dev, "clock-frequency", &clock);
| - clk_enable(&priv->ddr_clk);
| -+ sifive_ddr_setup(dev);
\ - dev_read_u32_array(dev, "sifive,ddr-params", (u32 *)&plat->ddr_params,
sizeof(plat->ddr_params) / sizeof(u32));
| - ... "ddr 硬件寄存器设置参数"
| - spl_clear_bss
| - spl_clear_bss_loop "初始化bss"
| -+ spl_stack_gd_setup
\ - jal    spl_relocate_stack_gd "什么也没干"
| -+ beqz   a0, spl_call_board_init_r
\ -+ jal    board_init_r(a0=zero,a1=zero)
\ -+ spl_set_bd();
\ - gd->bd = &bdata;
| - mem_malloc_init(CONFIG_SYS_SPL_MALLOC_START =0x3fe430000, CONFIG_SYS_SPL_MALLOC_SIZE); "堆内存相关"
| - memset(&spl_image, '\0', sizeof(spl_image));
| -+ board_boot_order(spl_boot_list); "调用到board 相关的"
\ -+ spl_boot_list[0] = spl_boot_device();
\ - mode_select = readl((void *)MODE_SELECT_REG);
| - if MODE_SELECT_SD: BOOT_DEVICE_MMC1 "默认和SD_SELECT 时为 BOOT_DEVICE_MMC1"
| -+ boot_from_devices(&spl_image, spl_boot_list, ARRAY_SIZE(spl_boot_list)
\ -+ for (i = 0; i < count && spl_boot_list[i] != BOOT_DEVICE_NONE; i++)
\ -+ loader = spl_ll_find_loader(spl_boot_list[i])
"定位到 SPL_LOAD_IMAGE_METHOD("MMC1", 0, BOOT_DEVICE_MMC1, spl_mmc_load_image);"
\ -+ spl_image_loader *drv = ll_entry_start(struct spl_image_loader, spl_image_loader);
\ - .u_boot_list_2_spl_image_loader_1 "定位到 该地址, 从 map section 找到的, 其函数为 spl_mmc_load_image"
| -+ int n_ents = ll_entry_count(struct spl_image_loader, spl_image_loader);
"从 u_boot_list_2_spl_image_loader_1 遍历到 u_boot_list_2_spl_image_loader_3"
| -+ for (entry = drv; entry != drv + n_ents; entry++)
\ - find boot_device == entry->boot_device
| -+ spl_load_image(spl_image, loader)
\ - bootdev.boot_device = loader->boot_device; "boot_device 为 BOOT_DEVICE_MMC1"
| -+ loader->load_image(spl_image, &bootdev); "调用到 spl_mmc_load_image"
\ -+ spl_mmc_load(spl_image, bootdev, CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_SECTOR = 0x822 = 2082)
"这个地方指到了 第二个分区的 sector 位置"
\ -+ spl_mmc_find_device(&mmc, bootdev->boot_device)
\ -+ mmc_init_device(0);
\ - uclass_get_device(UCLASS_MMC, num, &dev);
| - m = mmc_get_mmc_dev(dev); "mmc 驱动需要提前加载"
| - err = mmc_init(mmc); "mmc 初始化"
| -+ boot_mode = spl_mmc_boot_mode(bootdev->boot_device);
\ - return MMCSD_MODE_RAW
| - raw_sect = spl_mmc_get_uboot_raw_sector(mmc); "MMCSD_MODE_EMMCBOOT case 分支并没有break"
| -+ mmc_load_image_raw_sector(spl_image, mmc, raw_sect);
\ - blk_dread(bd, sector, 1, header); "从 mmc raw_sect 处 读入header"
| -+ spl_load_simple_fit(spl_image, &load, sector, header);
"镜像为FIT 格式时 header->ih_magic = FDT_MAGIC 的情况 , 加载到指定位置, 信息填充到 spl_image下"
\ -+ spl_simple_fit_read(&ctx, info, sector, fit) "uboot.itb 的 fit 描述信息"
\ - buf = board_spl_fit_buffer_addr(size, sectors, info->bl_len);
| - count = info->read(info, sector, sectors, buf); "从mmc 中读出 FIT blob"
| - ctx->fit = buf; "FIT blob 指针赋给ctx, ctx 后面使用会比较频繁"
| -+ node = spl_fit_get_image_node(&ctx, FIT_FIRMWARE_PROP, 0); "从uboot.itb 的FIT blob 获取firmware node"
\ -+ spl_fit_get_image_name(ctx, type, index, &str);
\ - name = fdt_getprop(ctx->fit, ctx->conf_node, type, &len);
"找 configurations node 下的firmware节点, 节点value 填充到str, 这里为 opensbi"
| - node = fdt_subnode_offset(ctx->fit, ctx->images_node, str);
"查image下的 name 为 opensbi 的节点"
| -+ spl_load_fit_image(info, sector, &ctx, node, spl_image);
"将opensbi node 节点下的信息读到 spl_image中"
"这里主要是填充load_addr, entry_point , 同时将数据下载到 load_addr 处"
\ - fit_image_get_data_offset(fit, node, &offset) "解析data-offset 节点, 获取bin offset"
| - fit_image_get_data_size(fit, node, &len) "解析data-size 节点, 获取opensbi bin的长度"
| - src_ptr = map_sysmem(ALIGN(load_addr, ARCH_DMA_MINALIGN), len); "分配内存, 这里直接指向load_addr处"
| - info->read(info, sector + get_aligned_image_offset(info, offset), nr_sectors, src_ptr)
"下载数据到src_ptr 处"
| - src = src_ptr + overhead;
| - fit_image_verify_with_data(fit, node, gd_fdt_blob(), src, length) "校验hash"
| - memcpy(load_ptr, src, length);
| - image_info->load_addr = load_addr; "填充spl_image"
| - fit_image_get_entry(fit, node, &entry_point) "查entry节点"
| - image_info->entry_point = entry_point; "填充spl_image"
| -+ node = spl_fit_get_image_node(&ctx, "loadables", index); "查loadables节点, 这个节点对应uboot"
| - spl_load_fit_image(info, sector, &ctx, node, &uboot_image_info); "下载uboot 节点的数据"
| -+ spl_fit_append_fdt(&uboot_image_info, info, sector, &ctx);
\ - node = spl_fit_get_image_node(ctx, FIT_FDT_PROP, index++); "下载fdt 节点数据"
| - image_info.load_addr = uboot_image_info->load_addr + uboot_image_info->size;
"fdt load_addr 追加到uboot镜像后面"
| - uboot_image_info->fdt_addr = map_sysmem(image_info.load_addr, 0);
| - spl_load_fit_image(info, sector, ctx, node, &image_info); "下载fdt 到 fdt_addr 处"
| - spl_image->fdt_addr = uboot_image_info.fdt_addr; "将fdt的load_addr 拷贝给 opensbi的spl_image 信息"
| - spl_image->boot_device = spl_boot_list[i];
| - switch (spl_image.os)
| -+ case IH_OS_OPENSBI:
\ -+ spl_invoke_opensbi(&spl_image); "跳到opensbi"
\ - check spl_image->fdt_addr valid
| - spl_opensbi_find_uboot_node(spl_image->fdt_addr, &uboot_node); "查fdt 属性 fit images 下是否有uboot的节点"
| - fit_image_get_entry(spl_image->fdt_addr, uboot_node, &uboot_entry);
"uboot 节点下的entry (运行地址)信息填给 uboot_entry"
| - opensbi_info.next_addr = uboot_entry;
| - opensbi_info.next_mode = FW_DYNAMIC_INFO_NEXT_MODE_S;
| - opensbi_info.boot_hart = gd->arch.boot_hart
| - opensbi_entry = (void (*)(ulong, ulong, ulong))spl_image->entry_point; "opensbi的entry 起始运行地址"
| - invalidate_icache_all(); "刷新icache"
| - opensbi_entry(gd->arch.boot_hart, (ulong)spl_image->fdt_addr, (ulong)&opensbi_info);
"跳到opensbi entry 运行起始地址, 携带了opensbi 下一跳的信息"

注意点:
spl 本身带有dtb, 这部分dtb 描述了spl 初始化外设的信息, 包括cpu ddr uart 等的参数
而uboot.itb 的dtb 会从mmc 存储中load 出来, 其描述信息会被解析用来确定firmware的信息.

加载的流程为:

  1. 定位到 (u-boot + opensbi + fdt) 镜像所在的分区, 注意这个地方现在是写的死的位置 2082 sector, 由 CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_SECTOR 定义
  2. 读取镜像的header, 判断ih_magic 是否是FIT格式还是legacy 模式
  3. FIT 格式的镜像由spl_load_simple_fit 函数加载
  4. 下载(opensbi uboot fdt) bin 数据 到其节点描述的load位置, 填充image_info结构, 保存各级 entry (运行起始地址) load 信息
  5. 跳转到opensbi, 此处opensbi 为 fw_dynamic, 需要携带下一跳 即uboot 的start_entry 和 fdt的load地址信息.

整个加载的核心代码在 spl_load_simple_fit 函数中.
对于不同的存储设备类型, 由spl_boot_device() 定义, 本例中默认为 BOOT_DEVICE_MMC1,
还有以下这些供选择. 对应不同的类型由 SPL_LOAD_IMAGE_METHOD 方法注册对应的load_image 的 ops 函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum {
BOOT_DEVICE_RAM,
BOOT_DEVICE_MMC1,
BOOT_DEVICE_MMC2,
BOOT_DEVICE_MMC2_2,
BOOT_DEVICE_NAND,
BOOT_DEVICE_ONENAND,
BOOT_DEVICE_NOR,
BOOT_DEVICE_UART,
BOOT_DEVICE_SPI,
BOOT_DEVICE_USB,
BOOT_DEVICE_SATA,
BOOT_DEVICE_I2C,
BOOT_DEVICE_BOARD,
BOOT_DEVICE_DFU,
BOOT_DEVICE_XIP,
BOOT_DEVICE_BOOTROM,
BOOT_DEVICE_NONE
};

不同device 的load_image的ops 对于FIT 镜像都会调用到 spl_load_simple_fit 函数.

在介绍该函数前, 需要先了解 uboot的FIT格式镜像, FIT(flattened image tree), 它利用了Device Tree Source files(DTS)的语法,生成的image文件也和dtb文件类似(称作itb),下面我们会详细描述

uboot.itb 中会集成 opensbi uboot-nodtb 和 dtb 三部分镜像, 先看下描述文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
dtc u-boot.dtb
...
binman {
multiple-images;

itb {
filename = "u-boot.itb";

fit {
description = "Configuration to load OpenSBI before U-Boot";
#address-cells = <0x1>;
fit,fdt-list = "of-list";

images {

uboot {
description = "U-Boot";
type = "standalone";
os = "U-Boot";
arch = "riscv";
compression = "none";
load = <0x80200000>;

blob-ext {
filename = "u-boot-nodtb.bin";
};
};

opensbi {
description = "OpenSBI fw_dynamic Firmware";
type = "firmware";
os = "opensbi";
arch = "riscv";
compression = "none";
load = <0x80000000>;
entry = <0x80000000>;

opensbi {
filename = "fw_dynamic.bin"; // filename 不是真实节点名
};
};

@fdt-SEQ {
description = "NAME";
type = "flat_dt";
compression = "none";
};
};

configurations {
default = "conf-1";

@conf-SEQ {
description = "NAME";
firmware = "opensbi";
loadables = "uboot";
fdt = "fdt-SEQ";
};
};
};
};
};

注意, 上面的filename 不是真实节点名称, 它类似于incbin, 在mkimage 打包时会解析该prop, 将prop 描述的文件塞到 itb 镜像中, 并记录其在镜像中的offset 和 size, 记为data-offset(或data-position) 和 data-size

pc host 端使用 ./tools/mkimage -l ./u-boot.itb 可以打印itb 中分区的信息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 ./tools/mkimage -l ./u-boot.itb
FIT description: Configuration to load OpenSBI before U-Boot
Created: Mon Nov 21 18:13:47 2022
Image 0 (uboot)
Description: U-Boot
Created: Mon Nov 21 18:13:47 2022
Type: Standalone Program
Compression: uncompressed
Data Size: 641008 Bytes = 625.98 KiB = 0.61 MiB
Architecture: RISC-V
Load Address: 0x80200000
Entry Point: unavailable
Image 1 (opensbi)
Description: OpenSBI fw_dynamic Firmware
Created: Mon Nov 21 18:13:47 2022
Type: Firmware
Compression: uncompressed
Data Size: 116016 Bytes = 113.30 KiB = 0.11 MiB
Architecture: RISC-V
OS: RISC-V OpenSBI
Load Address: 0x80000000
Image 2 (fdt-1)
Description: hifive-unmatched-a00
Created: Mon Nov 21 18:13:47 2022
Type: Flat Device Tree
Compression: uncompressed
Data Size: 22004 Bytes = 21.49 KiB = 0.02 MiB
Architecture: Unknown Architecture
Default Configuration: 'conf-1'
Configuration 0 (conf-1)
Description: hifive-unmatched-a00
Kernel: unavailable
Firmware: opensbi
FDT: fdt-1
Loadables: uboot

而u-boot 中可以使用iminfo itb 文件load到的地址 查看真实的镜像信息, 会包含 “data-offset” 和 “data-size” 节点

对于多核启动流程, 上述过程并未提及.
下面说下spl中其他核的处理过程:

从核启动过程

在上面的 spl_invoke_opensbi 函数中, 最后处理了其他核的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-+ ret = smp_call_function((ulong)spl_image->entry_point,
(ulong)spl_image->fdt_addr,
(ulong)&opensbi_info, wait=1);
\ - ipi_data ipi = { .addr = entry_point, .arg0 = fdt_addr, .arg1 = opensbi_info, };
| -+ send_ipi_many(&ipi, wait);
\ - cpus = ofnode_path("/cpus");
| -+ ofnode_for_each_subnode(node, cpus) "扫描spl的fdt"
\ - ofnode_read_u32(node, "reg", &reg); "read hart ID of CPU 放在reg中"
| - !gd->arch.available_harts & (1 << reg) | continue; "跳过not available的"
| - reg == gd->arch.boot_hart | continue; "跳过主核"
| - gd->arch.ipi[reg].addr = ipi->addr;
| - gd->arch.ipi[reg].arg0 = ipi->arg0;
| - gd->arch.ipi[reg].arg1 = ipi->arg1;
| - riscv_send_ipi(reg); "主核给其他核发ipi中断"
| -+ while pending
\ - wait | riscv_get_ipi(reg, &pending); "查是否pending, 如果从核没处理完该pending中断, 主核不会跳到下一阶段即 opensbi"

spl 中从核的初始化流程并没有设置 mstatus.mie mie, 虽然设置了mie.MSIE 和 mtvec trap entry, 但其中断并没打开.

虽然中断没打开, 但是可以被外部中断信号从wfi 处叫起来.

从核停在 secondary_hart_loop, 一直在loop, wfi停掉, 重点看下这个函数:

注意CONFIG_IS_ENABLED 宏, 与CONFIG_SPL_BUILD/ CONFIG_TPL_BUILD 联合使用, 这个BUILD宏是由Makefile spl传入的.

1
2
3
4
5
6
7
#if defined(CONFIG_TPL_BUILD)
#define _CONFIG_PREFIX TPL_
#elif defined(CONFIG_SPL_BUILD)
#define _CONFIG_PREFIX SPL_
#else
#define _CONFIG_PREFIX
#endif

if CONFIG_SPL_BUILD is undefined and CONFIG_FOO is set to ‘y’,
if CONFIG_SPL_BUILD is defined and CONFIG_SPL_FOO is set to ‘y’,
if CONFIG_TPL_BUILD is defined and CONFIG_TPL_FOO is set to ‘y’,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 0000000008000260 <secondary_hart_loop>:
8000260: 10500073 wfi
#if CONFIG_IS_ENABLED(SMP) "拆解为 CONFIG_SPL_SMP 是否定义了"
8000264: 344022f3 csrr t0,mip
8000268: 0082f293 andi t0,t0, MIE_MSIE //前面已经设置了 MIE_MSIE
800026c: fe028ae3 beqz t0,8000260 <secondary_hart_loop>
8000270: 8512 mv a0,tp
8000272: 3f4000ef jal ra,8000666 <handle_ipi> // 进入handle_ipi
#endif
8000276: b7ed j 8000260 <secondary_hart_loop>

void handle_ipi(ulong hart)
{
int ret;
void (*smp_function)(ulong hart, ulong arg0, ulong arg1);
// 检查gd->arch.ipi[hart].valid, 在主核没发起ipi 前, 这个地方是无效的, 会直接退出 handle_ipi 回到 secondary_hart_loop
if (!__smp_load_acquire(&gd->arch.ipi[hart].valid))
return;

smp_function = (void (*)(ulong, ulong, ulong))gd->arch.ipi[hart].addr;
invalidate_icache_all();

/*
* Clear the IPI to acknowledge the request before jumping to the
* requested function.
*/
ret = riscv_clear_ipi(hart);
if (ret) {
pr_err("Cannot clear IPI of hart %ld (error %d)\n", hart, ret);
return;
}
// 执行主核设置的参数, 这个地方跳转到 opensbi 入口地址, arg0为fdt_addr, arg1 为 opensbi_info
smp_function(hart, gd->arch.ipi[hart].arg0, gd->arch.ipi[hart].arg1);
}

在主核给从核发送ipi后, 从核从wfi中唤醒, 进入handle_ipi中, 跳入opensbi, 设置a0 寄存器为 uboot.itb 中带的fdt的下载到的地址fdt_addr, a1 寄存器为 opensbi_info, 携带了opensbi下一级的启动信息(uboot_entry majic version boot_hart等)

opensbi 启动流程

spl 加载uboot.itb, 将opensbi 镜像下载到 ddr 0x80000000 位置, uboot 下载到0x80200000 位置
然后启动dynamic 的opensbi, 这里不一定是dynamic的, 可以定制为payload或jump的.
下面以dynamic 为例分析 opensbi的启动流程.

首先看下opensbi的布局:

img

从其布局中需要重点了解 属于hartid的 scratch space, 这个空间在启动过程以及消息传递过程会频繁用到.
下面看下启动过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
_start:
/* Find preferred boot HART id */
MOV_3R s0, a0, s1, a1, s2, a2 "a0 为hartid"
call fw_boot_hart "从spl 传递的opensbi_info 中找boot_hart 给到 a0 -> a6, 如果opensbi不是fw_dynamic的, a0->a6 为 -1"
add a6, a0, zero
MOV_3R a0, s0, a1, s1, a2, s2
li a7, -1
beq a6, a7, _try_lottery "fw_jump 情况下, a0=a6=-1, 主核进入_try_lottery, 从核也会进_try_lottery"
/* Jump to relocation wait loop if we are not boot hart */
bne a0, a6, _wait_relocate_copy_done "fw_dynamic a0=a6=0, 主核进入 _try_lottery, 从核a0 > 0, 进入_wait_relocate_copy_done. "
_try_lottery:
/* Jump to relocation wait loop if we don't get relocation lottery */
lla a6, _relocate_lottery
li a7, 1
amoadd.w a6, a7, (a6) "主核先抢到lottery, 不会进入_wati_relocate_copy_done, 从核进入_wait_relocate_copy_done"
bnez a6, _wait_relocate_copy_done

/* Save load address */
lla t0, _load_start
lla t1, _fw_start
REG_S t1, 0(t0)

relocate:
...

_relocate_done:
lla    t0, _boot_status
    li  t1, BOOT_STATUS_RELOCATE_DONE
    REG_S   t1, 0(t0) "设置_boot_status = BOOT_STATUS_RELOCATE_DONE = 1"
 
_fdt_reloc_done:
/* mark boot hart done */
li t0, BOOT_STATUS_BOOT_HART_DONE
lla t1, _boot_status
REG_S t0, 0(t1) "设置_boot_status = BOOT_STATUS_BOOT_HART_DONE = 2"
fence rw, rw
j _start_warm "主核relocate完 设置BOOT_STATUS_BOOT_HART_DONE = 2 后进入 _start_warm"
   
_wait_relocate_copy_done:
lla t0, _fw_start
lla t1, _link_start
REG_L t1, 0(t1)
beq t0, t1, _wait_for_boot_hart "进_wait_for_boot_hart"
lla t2, _boot_status
lla t3, _wait_for_boot_hart
sub t3, t3, t0
add t3, t3, t1 "t3 = _wait_for_boot_hart - _fw_start + _link_start"
1:
/* waitting for relocate copy done (_boot_status == 1) */
li t4, BOOT_STATUS_RELOCATE_DONE
REG_L t5, 0(t2)
/* Reduce the bus traffic so that boot hart may proceed faster */
nop
nop
nop
bgt t4, t5, 1b "等 _boot_status == (BOOT_STATUS_RELOCATE_DONE = 1)"
jr t3 "跳到 _wait_for_boot_hart"

_wait_for_boot_hart:
li t0, BOOT_STATUS_BOOT_HART_DONE
lla t1, _boot_status
REG_L t1, 0(t1)
/* Reduce the bus traffic so that boot hart may proceed faster */
nop
nop
nop
bne t0, t1, _wait_for_boot_hart "等 _boot_status == (BOOT_STATUS_BOOT_HART_DONE = 2)"

_start_warm: "主从核在 BOOT_STATUS_BOOT_HART_DONE = 2 后进入 _start_warm"
lla    a4, platform
    lwu s7, SBI_PLATFORM_HART_COUNT_OFFSET(a4) "从platform 中获取hart数量, ->s7"
    lwu s8, SBI_PLATFORM_HART_STACK_SIZE_OFFSET(a4) "获取stack_size, ->s8"
    csrr   s6, CSR_MHARTID "读取hartid ->s6"
    "Find the scratch space based on HART index"
    lla tp, _fw_end
mul a5, s7, s8
add tp, tp, a5 "_fw_end + hart_cout * hart_stack_size -> tp"
mul a5, s8, s6
sub tp, tp, a5 "_fw_end + (hart_cout - hartid) * hart_stack_size -> tp
"为 hartid 所在的hart_stack空间"
li a5, SBI_SCRATCH_SIZE
sub tp, tp, a5 "hartid hart_stack - SBI_SCRATCH_SIZE 为 hartid的scratch 所在地址"
    csrw   CSR_MSCRATCH, tp "将 hartid的scratch 所在地址 赋给 mscratch"
lla    a4, _trap_handler "设置M-mode trap 入口为 _trap_handler"
csrw   CSR_MTVEC, a4
csrr a0, CSR_MSCRATCH
call sbi_init "主从核调用 sbi_init(mscratch)"

platform 为平台定义, 定义了平台相关信息

1
2
3
4
5
6
7
8
9
10
11
12
struct sbi_platform platform = {
.opensbi_version = OPENSBI_VERSION,
.platform_version =
SBI_PLATFORM_VERSION(CONFIG_PLATFORM_GENERIC_MAJOR_VER,
CONFIG_PLATFORM_GENERIC_MINOR_VER),
.name = CONFIG_PLATFORM_GENERIC_NAME,
.features = SBI_PLATFORM_DEFAULT_FEATURES,
.hart_count = SBI_HARTMASK_MAX_BITS,
.hart_index2id = generic_hart_index2id,
.hart_stack_size = SBI_PLATFORM_DEFAULT_HART_STACK_SIZE,
.platform_ops_addr = (unsigned long)&platform_ops
};

主从核进入 sbi_init 的c_code 后续处理流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
-+ sbi_init(mscratch)  "mscratch 地址指向hartid 所在的 scratch space"
\ - u32 hartid         = current_hartid(); "csr_read(CSR_MHARTID)"
| - sbi_platform *plat = sbi_platform_ptr(scratch);
| -+ next_mode_supported && atomic_xchg(&coldboot_lottery, 1) == 0
"主核抢到 coldboot_lottery, 设置前为0, 设置后为1, 从核抢不到, coldboot = FALSE"
\ - coldboot = TRUE "主核 coldboot 为 TRUE"
| -|+ if coldboot
\ -+ init_coldboot(scratch, hartid); "主核进"
\ - sbi_platform *plat = sbi_platform_ptr(scratch);
| - sbi_scratch_init(scratch);
| - sbi_domain_init(scratch, hartid);
| - sbi_hsm_init(scratch, hartid, TRUE);
| - sbi_platform_early_init(plat, TRUE);
| - sbi_hart_init(scratch, TRUE);
| - sbi_console_init(scratch);
| - sbi_pmu_init(scratch, TRUE);
| - sbi_irqchip_init(scratch, TRUE);
| - sbi_ipi_init(scratch, TRUE);
| - sbi_tlb_init(scratch, TRUE);
| - sbi_timer_init(scratch, TRUE);
| - sbi_ecall_init();
| - sbi_platform_final_init(plat, TRUE);
| - wake_coldboot_harts(scratch, hartid); "Send an IPI to all HARTs waiting for coldboot"
| - sbi_hsm_prepare_next_jump(scratch, hartid);
| - sbi_hart_switch_mode(hartid, scratch->next_arg1, scratch->next_addr,
scratch->next_mode, FALSE); "主核进下一级 uboot, 跳入uboot_entry, 携带uboot.itb中的dtb下到的地址"
-|+ else !codeboot
\ -+ init_warmboot(scratch, hartid); "其他从核进"
\ - wait_for_coldboot(scratch, hartid); "等主核 wake_coldboot_harts"
| - hstate = sbi_hsm_hart_get_state(sbi_domain_thishart_ptr(), hartid);
"从 hartid_to_domain_table[__hartid] 中拿hart的初始hsm_state"
| -|+ if (hstate == SBI_HSM_STATE_SUSPENDED)
\ - init_warm_resume(scratch); "从核是suspend状态时, 即从核从挂起状态唤醒, 走resume"
| -|+ else != SBI_HSM_STATE_SUSPENDED "第一次起来时为 SBI_HSM_STATE_STOPED 状态"
\ -+ init_warm_startup(scratch, hartid); "从核第一次boot时, 走startup"
\ - sbi_platform *plat = sbi_platform_ptr(scratch);
| -+ sbi_hsm_init(scratch, hartid, FALSE);
\ -+ sbi_hsm_hart_wait(scratch, hartid);
\ - csr_set(CSR_MIE, MIP_MSIP | MIP_MEIP); "Set MSIE and MEIE bits to receive IPI"
\ -+ while (atomic_read(&hdata->state) != SBI_HSM_STATE_START_PENDING)
\ - wfi(); "从核的opensbi第一次boot会停在这里, 等待接收ipi, 但是ipi 处理函数并不会置 state,
只有ecall SBI_EXT_HSM_HART_START才会设置state 为 SBI_HSM_STATE_START_PENDING,并发起ipi"
| - sbi_platform_early_init(plat, FALSE);
| - sbi_hart_init(scratch, FALSE);
| - sbi_pmu_init(scratch, FALSE);
| - sbi_irqchip_init(scratch, FALSE);
| - sbi_ipi_init(scratch, FALSE);
| - sbi_tlb_init(scratch, FALSE);
| - sbi_timer_init(scratch, FALSE);
| - sbi_platform_final_init(plat, FALSE);
| - sbi_hsm_prepare_next_jump(scratch, hartid);
"检查hsm_state 是否为 SBI_HSM_STATE_START_PENDING,
如果是的话, 设置为 SBI_HSM_STATE_STARTED; 如果不是 将从核hang住"
| - sbi_hart_switch_mode(hartid, scratch->next_arg1, scratch->next_addr, scratch->next_mode, FALSE);
"从核跳下一级, 注意ecall SBI_EXT_HSM_HART_START 的消息会重新设置 scratch的 next_arg1, next_addr, next_mode"

主从核的boot 走了不同的分支, 主核走init_coldboot, 从核走init_warm_startup
只有主核才跳入了uboot, 从核停在了检查hsm_state 为 SBI_HSM_STATE_START_PENDING的地方.
只有主核发 ecall SBI_EXT_HSM_HART_START, 主核设置了从核hartid 所属的scratch space中的hsm_state为 SBI_HSM_STATE_START_PENDING , 再把从核叫起后, 从核才会接着往下走.
ecall SBI_EXT_HSM_HART_START 的消息会重新设置 scratch的 next_arg1, next_addr, next_mode, 对应kernel的情况, 将next_arg1 设置为了 kernel的封装的私有数据地址, next_addr 设置为 secondary_start_sbi 地址, next_mode 设置为S-mode.
scratch的设置最终会影响到 sbi_hart_switch_mode 函数跳入下一级.

从目前的逻辑看, 从核貌似并没有机会进uboot, uboot里面只是发了ipi中断, 并没有ecall SBI_EXT_HSM_HART_START 的过程.
从核会直接跳到kernel中. 从逻辑上看, 从核也没有必要进uboot, 因为uboot是S-mode的, 它只是一个中间态.

启动链

sifive的信息, opensbi 0x80000000 uboot 0x80200000 kernel 先load到 0x84000000, relocate 到 0x80200000 执行
kernel 先从物理地址上运行, 执行relocate_enable_mmu 后, 再从虚拟地址执行
kernel 的map信息记录的都是虚拟地址, 在enable_mmu 之前, 只能进行相对跳转.

kernel before mmu 加载流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
__HEAD
ENTRY(_start)
/*
* Image header expected by Linux boot-loaders. The image header data
* structure is described in asm/image.h.
* Do not modify it without modifying the structure and all bootloaders
* that expects this header format!!
*/
/* jump to start kernel */
j _start_kernel
/* reserved */
.word 0
.balign 8
#if __riscv_xlen == 64
/* Image load offset(2MB) from start of RAM */
.dword 0x200000
#else
/* Image load offset(4MB) from start of RAM */
.dword 0x400000
#endif
/* Effective size of kernel image */
.dword _end - _start
.dword __HEAD_FLAGS
.word RISCV_HEADER_VERSION
.word 0
.dword 0
.ascii RISCV_IMAGE_MAGIC
.balign 4
.ascii RISCV_IMAGE_MAGIC2
.word 0

.head.text section 开头为构建的PE header,
我们可以找到RISC-V架构的启动协议Documentation/riscv/boot-image-header.txt。事实上这个文件只是简单介绍了一下Image文件header的结构,更详细的启动协议还是处于TODO状态,我们需要从代码进行分析。前面提到RISC-V内核比较类似于ARM64内核的格式,如下:

1
2
3
4
u32 code0;                /* Executable code */
u32 code1; /* Executable code */
u64 text_offset; /* Image load offset, little endian */
u64 image_size; /* Effective Image size, little endian */

从bootloader的角度,装载并执行Image类型的内核只需要做两件事情:

  • 将整个Image文件放置到内存起始处向后偏移text_offset的内存地址
  • 跳转到code0的地址进行执行

后续的事情,内核自理。我们看到head.S中有如下代码:
code0code1放置的就是j _start_kernel生成的指令。那么我们就需要从_start_kernel开始看起。

_start_kernel可以看到,一开始主要做了三件事:

  1. 关闭所有的中断
  2. 设置gp寄存器指向对应的地址(该寄存器为ABI相关,用于存放__global_pointer$的地址,即GOT的位置,可以参考RISCV调用协定
  3. 关闭FPU,内核中是不用任何浮点指令的

bootloader将控制权交给内核时,a0寄存器保存的值即为当前CPU执行单元的ID(RISC-V中称为HART ID)。
_start_kernel接下来就开始执行一个简单的Boot Protocol,选出一个用于启动内核的CPU,其他CPU进入等待状态。RISC-V处理器在reset之后,所有的处理单元(HART)都会一起开始执行,而Linux内核启动时为主从模型,因此需要挑选出其中一个完成部分内核启动工作,之后告知其他的处理器继续执行开始处理任务。首先确定CPU ID是否合法,即有没有超出内核编译时选择的最大支持CPU数,如果超过则非法:

1
2
3
4
5
6
#ifdef CONFIG_SMP
li t0, CONFIG_NR_CPUS
blt a0, t0, .Lgood_cores
tail .Lsecondary_park
.Lgood_cores:
#endif

.Lsecondary_park分支实质上是循环调用wfi指令。接下来内核使用一个简单的策略选出用于启动的主CPU:先到先得。setup.c中定义了一个原子变量:

1
atomic_t hart_lottery __section(.sdata);

所有CPU都会试图通过原子操作将这个变量加1。RISC-V的原子操作指令会将原子变量的原有值保存在原子变量的目标寄存器中,也就是说,如果操作后目标寄存器的值为0的CPU为第一对其进行操作的CPU。

1
2
3
4
5
/* Pick one hart to run the main boot sequence */
la a3, hart_lottery
li a2, 1
amoadd.w a3, a2, (a3)
bnez a3, .Lsecondary_start

对于所有竞争失败的CPU,我们在后面进行分析,目前还是顺着主CPU进行分析。随后主CPU的操作基本如下:

  • 清空bss段,写为0
  • 设置临时内核栈与task_struct
  • 依次调用setup_vm以及relocate_enable_mmu设置内核的虚拟内存
    调用完 relocate_enable_mmu 后, mmu 已经准备完, 此时已经为整个kernel 运行空间建立了完整的页表映射.
    _start_end 0xffffffff80000000 - 0xffffffff81345000

最后调用C语言通用代码启动内核:

1
2
3
4
5
6
la tp, init_task "init_task 初始的 task_struct, 即内核的第一个任务(`init/init_task.c`)"
la sp, init_thread_union + THREAD_SIZE
"init_thread_union是通过链接脚本留出的一个PAGE(riscv平台上为4KB)大小的区域,用以充当临时内核栈"
/* Start the kernel */
call soc_early_init
tail start_kernel "启动内核"

tp 寄存器的作用: Supervisor模式下的用途:保存当前CPU运行的进程上下文,即task_struct结构体。查看asm/current.h

1
2
3
4
5
create_pte_mappingstatic __always_inline struct task_struct *get_current(void)
{
register struct task_struct *tp __asm__("tp");
return tp;
}

setup_vm

将内核所在的物理地址映射到PAGE_OFFSET所在虚拟地址区域
(即将内核二进制所在的物理地址加上一个PAGE_OFFSET减去_start所的的偏移量)。因此,这里的任务实质上是建立一个临时页表,即内核临时的虚拟地址空间映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
mv s1, a1    "a1 寄存器是opensbi  传过来的fdt 的物理地址"
mv a0, s1
call setup_vm
-+ setup_vm(dtb_pa)
\ - kernel_map.virt_addr = KERNEL_LINK_ADDR ; "0xffffffff800000000"
| - kernel_map.page_offset = _AC(CONFIG_PAGE_OFFSET, UL); "0xff60000000000000"
| - kernel_map.phys_addr = (uintptr_t)(&_start); "0x80200000"
| - kernel_map.size = (uintptr_t)(&_end) - kernel_map.phys_addr; "kernel bin size"
| - BUG_ON((kernel_map.phys_addr % PMD_SIZE) != 0); "检查物理地址2M字节对齐"
| -+ set_satp_mode(); "检查是否支持4级页表"
| - kernel_map.va_pa_offset = PAGE_OFFSET - kernel_map.phys_addr; "0xff60000000000000 - 0x80200000"
| - kernel_map.va_kernel_pa_offset = kernel_map.virt_addr - kernel_map.phys_addr;
"0xffffffff800000000 - 0x80200000"
| - riscv_pfn_base = PFN_DOWN(kernel_map.phys_addr); "右移一个page 0x80200"
| -+ pt_ops_set_early();
\ - pt_ops.alloc_pte = alloc_pte_early; " allc_pte_early里面是BUG(),对于临时页表,kernel不允许我们建立PTE
临时页表, 只用到大页"
| - pt_ops.get_pte_virt = get_pte_virt_early;
| - create_pgd_mapping(early_pg_dir, FIXADDR_START, fixmap_p4d, PGDIR_SIZE, PAGE_TABLE);
"FIXADDR_START 0xff1bfffffee00000 - fixmap_p4d pa 映射, early_pg_dir为基址"
| - create_p4d_mapping(fixmap_p4d, FIXADDR_START, fixmap_pud, P4D_SIZE, PAGE_TABLE);
| - create_pud_mapping(fixmap_pud, FIXADDR_START, fixmap_pmd, PUD_SIZE, PAGE_TABLE);
| - create_pmd_mapping(fixmap_pmd, FIXADDR_START, fixmap_pte, PMD_SIZE, PAGE_TABLE);
"建立了0xff1bfffffee00000 开始的 2M的大页 FIXADDR_START - fixmap_pte 映射"
| - create_pgd_mapping(trampoline_pg_dir, kernel_map.virt_addr,
trampoline_p4d, PGDIR_SIZE, PAGE_TABLE);
| - create_p4d_mapping(trampoline_p4d, kernel_map.virt_addr,
trampoline_pud, P4D_SIZE, PAGE_TABLE);
| - create_pud_mapping(trampoline_pud, kernel_map.virt_addr,
trampoline_pmd, PUD_SIZE, PAGE_TABLE);
| - create_pmd_mapping(trampoline_pmd, kernel_map.virt_addr,
kernel_map.phys_addr, PMD_SIZE, PAGE_KERNEL_EXEC);
"建立了 trampoline_pg_dir 为pgd 的 指向 kernel_map.virt_addr (0xffffffff800000000) 开始的2M的大页
kernel_map.virt_addr - kernel_map.phys_addr 0xffffffff800000000 - 0x80200000 映射"
| -+ create_kernel_page_table(early_pg_dir, true);
"建立了以 early_pg_dir 为pgd的页表映射, 为整个kernel bin 建立连续页表映射 early_pg_dir 地址 0x80c08000
0xffffffff800000000 - 0x80200000 映射"
\ - end_va = kernel_map.virt_addr + kernel_map.size;
| -+ for (va = kernel_map.virt_addr; va < end_va; va += 2M)
\ - create_pgd_mapping(pgdir, va, "va 虚拟地址"
kernel_map.phys_addr + (va - kernel_map.virt_addr), "对应的物理地址"
PMD_SIZE, "2M"
early ? PAGE_KERNEL_EXEC : pgprot_from_va(va)); "PAGE_KERNEL_EXEC 表示大页, 一个大页是2M"
| - create_fdt_early_page_table(early_pg_dir, dtb_pa); "为dtb 创建页表"
| -+ pt_ops_set_fixmap()
\ - pt_ops.alloc_pte = kernel_mapping_pa_to_va((uintptr_t)alloc_pte_fixmap);
| - pt_ops.get_pte_virt = kernel_mapping_pa_to_va((uintptr_t)get_pte_virt_fixmap);

上述过程中可以了解到最简单的一种建立五级页表映射的代码: pgd 为 trampoline_pg_dir, 建立虚拟地址 kernel_map.virt_addr 同 物理地址 kernel_map.phys_addr 的映射关系, 翻译过程为 satp-> trampoline_pg_dir -> trampoline_p4d -> trampoline_pud -> trampoline_pmd

1
2
3
4
5
6
7
8
| -  create_pgd_mapping(trampoline_pg_dir, kernel_map.virt_addr, 
trampoline_p4d, PGDIR_SIZE, PAGE_TABLE);
| - create_p4d_mapping(trampoline_p4d, kernel_map.virt_addr,
trampoline_pud, P4D_SIZE, PAGE_TABLE);
| - create_pud_mapping(trampoline_pud, kernel_map.virt_addr,
trampoline_pmd, PUD_SIZE, PAGE_TABLE);
| - create_pmd_mapping(trampoline_pmd, kernel_map.virt_addr,
kernel_map.phys_addr, PMD_SIZE, PAGE_KERNEL_EXEC);

建立映射时传入了 PGDIR_SIZE P4D_SIZE PUD_SIZE PMD_SIZE 这几个size 导致 create_pgd_mapping create_p4d_mapping create_pud_mapping create_pmd_mapping 函数只填充了数据里的元素, 并没有走后面的流程.

以 create_pgd_mapping 为例, 其中 PAGE_TABLE 表示页目录, PAGE_KERNEL_EXEC 表示该级为叶子页表项

1
2
3
4
5
6
7
   uintptr_t pgd_idx = pgd_index(va);
if (sz == PGDIR_SIZE) {
if (pgd_val(pgdp[pgd_idx]) == 0)
pgdp[pgd_idx] = pfn_pgd(PFN_DOWN(pa), prot);
return;
}
...

sv48 和 sv57 对应 4level 和 5level mmu 映射:

线性地址 mmu 层级 Linux user address space Linux kernel address space
Sv48 (48bit) 4level: pgd→pud→pmd→pte→page(4k) 0x00000000 00000000 - 0x00007FFF FFFFFFFF 0xFFFF8000 00000000 - 0xFFFFFFFF FFFFFFFF
Sv57 (57bit) 5level: pgd→p4d→pud→pmd→pte→page(4k) 0x00000000 00000000 - 0x00FFFFFF FFFFFFFF 0xFF000000 00000000 - 0xFFFFFFFF FFFFFFFF

in sv57: PMD_SIZE = 1<<21 PUD_SIZE = 1<<30 P4D_SIZE = 1<<39 PGDIR_SIZE = 1<<48

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void __init create_pgd_mapping(pgd_t *pgdp,
uintptr_t va, phys_addr_t pa,
phys_addr_t sz, pgprot_t prot)
{
pgd_next_t *nextp;
phys_addr_t next_phys;
uintptr_t pgd_idx = pgd_index(va);

if (pgd_val(pgdp[pgd_idx]) == 0) {
next_phys = alloc_pgd_next(va); // 调用 pt_ops.alloc_p4d(__va), 返回 early_p4d
pgdp[pgd_idx] = pfn_pgd(PFN_DOWN(next_phys), PAGE_TABLE);
nextp = get_pgd_next_virt(next_phys); // pt_ops.get_p4d_virt return early_p4d
memset(nextp, 0, PAGE_SIZE);
} else {
next_phys = PFN_PHYS(_pgd_pfn(pgdp[pgd_idx]));
nextp = get_pgd_next_virt(next_phys);
}

create_pgd_next_mapping(nextp, va, pa, sz, prot);
create_p4d_mapping(__nextp, __va, __pa, __sz, __prot)
create_pud_mapping(nextp, va, pa, sz, prot);
create_pmd_mapping(nextp, va, pa, sz, prot);
create_pte_mapping(ptep, va, pa, sz, prot);
}
}

static void __init create_pmd_mapping(pmd_t *pmdp,
uintptr_t va, phys_addr_t pa,
phys_addr_t sz, pgprot_t prot)
{
pte_t *ptep;
phys_addr_t pte_phys;
uintptr_t pmd_idx = pmd_index(va);

if (sz == PMD_SIZE) { //这个地方满足条件 sz=PMD_SIZE=2M
if (pmd_none(pmdp[pmd_idx]))
pmdp[pmd_idx] = pfn_pmd(PFN_DOWN(pa), prot); "赋值大页"
return;
}
...
}

对于sv57 模式, pgd -> p4d -> pud -> pmd -> pte

每一级页 page 4k, 每个页项 占8个字节, 所以每一级可以代表 4k/8 = 512个页项
一个pte 项可以索引 4k
对于pmd 可表示 4k * 512 = 2M 地址字节
对于pud 可表示 4k * 512 * 512 = 1G 地址字节

对于kernel 本身而言, 其 bin 文件大概在128M 之内, 对于bin对应的连续物理地址, pmd 前面的pgd p4d pud仅需要一个页项就可以能表示这些物理地址了, 所以前面建页表过程可以看到只有一个early_pg_dir early_p4d early_pud, 到pmd 这一级才会分多个 pmd 多个pte 出来.

sv57 in RV64:

  • satp.PPN 给出了一级页表的基址, VA[56:48]给出了一级页号, 因此处理器会读取位于地址(satp.PPN × 4096 + VA[56:48] × 8)的页表项 pgd
  • pgd.PPN 给出了二级页表的基址, VA[47:39]给出了二级页号, 因此处理器会读取位于地址(pgd.PPN × 4096 + VA[47:39] × 8)的页表项 p4d
  • p4d.PPN 给出了三级页表的基址, VA[38:30]给出了三级页号, 因此处理器会读取位于地址(p4d.PPN × 4096 + VA[38:30] × 8)的页表项 pud
  • pud.PPN 包含四级页表的基址, VA[29:21]给出了四级页号, 因此处理器读取位于地址(pud.PPN × 4096 + VA[29:21] × 8)的页表项 pmd
  • pmd.PPN 包含了五级页表的基址, VA[20:12] 给出了五级页号, 处理器读取位于地址(pmd.PPN × 4096 + VA[20:12] × 8)的页表项 pte
  • 该页表项的PTE. PPN 就是物理地址对应的PPN * 4096 + VA[offset 11:0]得到物理地址
    sv57 上. 虚拟地址范围 57 位, 0-56 位 , 物理地址范围 56位(44位的PPN + 12位的offset), 0-55 位

上述页表建立过程中用到了大页, 在pmd 一级中, 最后的prot 赋值为了 PAGE_KERNEL_EXEC,
其组合为 V | R | A | W | G | X | D

1
_PAGE_READ | _PAGE_WRITE | _PAGE_PRESENT | _PAGE_ACCESSED | _PAGE_DIRTY | _PAGE_GLOBAL | _PAGE_EXEC

PTE 描述: 对应(pgd/p4d/pud/pmd/pte)项
image-20240416113156489
启用大页时, 在页项 V=1, 且RWX 不为0时, 表示为叶页表项
对于pmd 一级大页, pmd.PPN 包含了五级页表的基址, 地址(pmd.PPN × 4096 + VA[20:0]) 即是 va 对应的 物理地址.
pmd 大页可索引2M的地址字节.

setup_vm()在最开始就进行了kernel入口地址的对齐检查,要求入口地址2M对齐。假设内存起始地址为0x80000000,那么kernel只能放在0x80000000、0x80200000等2M对齐处。为什么会有这种对齐要求呢?
应该是为给opensbi预留了2M空间,因为kernel之前还有opensbi,而opensbi运行完之后,默认跳转地址就是偏移2M,kernel只是为了跟opensbi对应,所以设置了2M对齐。
opensbi需要占用2M这么大?实际上只需要几百KB,因此opensbi和kernel中间有一段内存是空闲的,没有人使用。

上述setup_vm 操作最主要的是建立了kernel的临时页表, 页表遍历需要从early_pg_dir 的pgd 开始

relocate_enable_mmu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
la a0, early_pg_dir
relocate_enable_mmu:
la a1, kernel_map "a1 - 0xffffffff80da0198 - 0xffffffff800000000 + 0x80200000 = 0x80fa0198"
XIP_FIXUP_OFFSET a1
REG_L a1, KERNEL_MAP_VIRT_ADDR(a1) "load a1, "a1 变为 0xffffffff80000000""
la a2, _start "a2 - 0x80200000"
sub a1, a1, a2 "a1 = a1-a2, a1 为 offset 虚拟地址与物理地址 offset"
add ra, ra, a1 "ra = ra + offset, 这个函数返回时就会用虚拟地址了, 所以直接+上面的offset由物理地址变到物理地址"

/* Point stvec to virtual address of intruction after satp write */
la a2, 1f "a2 为 label 1 物理地址"
add a2, a2, a1 "a2 = a2 + offset"
csrw CSR_TVEC, a2 "a2 0xffffffff80001048 写入stvec"

/* Compute satp for kernel page tables, but don't load it yet */
srl a2, a0, PAGE_SHIFT "a2 = a0 >> PAGE_SHIFT 构造PPN, a0 为 early_pg_dir"
la a1, satp_mode "satp_mode 中保存分页模式 为sv39/sv48/sv57等"
REG_L a1, 0(a1) "取出satp_mode 中存的 分页模式"
or a2, a2, a1 "分页模式 | ASID(NULL) | PPN " "a2 MODE(0xa0) | ASID(0) | PPN(0x80c08)" 为sv57模式

/*
* Load trampoline page directory, which will cause us to trap to
* stvec if VA != PA, or simply fall through if VA == PA. We need a
* full fence here because setup_vm() just wrote these PTEs and we need
* to ensure the new translations are in use.
*/
la a0, trampoline_pg_dir "a0 为 trampoline_pg_dir 物理地址 0x814e9000"
XIP_FIXUP_OFFSET a0
srl a0, a0, PAGE_SHIFT "取出页号 PPN 为 0x814e9"
or a0, a0, a1 "trampoline_pg_dir satp 分页模式 | ASID(NULL) | PPN"
"a0 MODE(0xa0) | ASID(0) | PPN(0x814e9)"
sfence.vma
csrw CSR_SATP, a0 "启用trampoline_pg_dir pgd 页表, 这个只映射了 kernel bin的前2M 数据"
1:
/* Set trap vector to spin forever to help debug */
la a0, .Lsecondary_park
csrw CSR_TVEC, a0
/* Reload the global pointer */
.option push
.option norelax
la gp, __global_pointer$
.option pop

/*
* Switch to kernel page tables. A full fence is necessary in order to
* avoid using the trampoline translations, which are only correct for
* the first superpage. Fetching the fence is guaranteed to work
* because that first superpage is translated the same way.
*/
csrw CSR_SATP, a2 "启用 early_pg_dir pgd 页表, 映射了完整的kernel bin"
sfence.vma

ret

临时页表分析

MMU开启前,需要建立好kernel、dtb、trampoline等页表。以便MMU开启后,并且在内存管理模块运行之前,kernel可以正常初始化,dtb可以正常地被解析。这部分页表都是临时页表,最终的页表在setup_vm_final()建立。

临时页表创建顺序:

首先为fixmap创建早期的PGD、PMD,这时PGD使用early_pg_dir。然后对从kernel开始的前2M内存建立二级页表,此时PGD使用trampoline_pg_dir,为这2M建立的页表也叫作superpage。再然后,对整个kernel创建二级页表,此时PGD使用early_pg_dir。最后为dtb预留4M大小创建二级页表。

内存布局:

1
2
3
4
5
6
7
[    0.000000]       fixmap : 0xff1bfffffee00000 - 0xff1bffffff000000   (2048 kB)
[ 0.000000] pci io : 0xff1bffffff000000 - 0xff1c000000000000 ( 16 MB)
[ 0.000000] vmemmap : 0xff1c000000000000 - 0xff20000000000000 (1024 TB)
[ 0.000000] vmalloc : 0xff20000000000000 - 0xff60000000000000 (16384 TB)
[ 0.000000] lowmem : 0xff60000000000000 - 0xff6000007fe00000 (2046 MB)
[ 0.000000] modules : 0xffffffff01345000 - 0xffffffff80000000 (2028 MB)
[ 0.000000] kernel : 0xffffffff80000000 - 0xffffffffffffffff (2047 MB)

附录:
relocate_enable_mmu 汇编分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
0x80201000:  auipc   a1,0xd9f
0x80201004: addi a1,a1,408 "a1 0x80fa0198"
0x80201008: ld a1,8(a1) "a1 = 0xffffffff80000000"
0x8020100a: auipc a2,0xfffff
0x8020100e: addi a2,a2,-10 "a2 = 0x80200000"
0x80201012: sub a1,a1,a2 "a1 0xfffffffeffe00000 虚拟地址与物理地址的offset"
0x80201014: add ra,ra,a1 "ra 0xffffffff80001134"
0x80201016: auipc a2,0x0
0x8020101a: addi a2,a2,50
0x8020101e: add a2,a2,a1
0x80201020: csrw stvec,a2 "a2 0xffffffff80001048 写入stvec"
0x80201024: srli a2,a0,0xc "a0 0x80c08000 a2 0x80c08"
0x80201028: auipc a1,0xd9f
0x8020102c: addi a1,a1,432 "a1 0x80fa01d8 存satp_mode的地址"
0x80201030: ld a1,0(a1) "satp_mode 0xa000000000000000"
0x80201032: or a2,a2,a1 "a2 MODE(0xa0) | ASID(0) | PPN(0x80c08)"
0x80201034: auipc a0,0x12e8
0x80201038: addi a0,a0,-52 "a0 trampoline_pg_dir 0x814e9000 - 0x80200000 实际位置"
0x8020103c: srli a0,a0,0xc "a0 0x814e9"
0x8020103e: or a0,a0,a1 "a0 MODE(0xa0) | ASID(0) | PPN(0x814e9)"
0x80201040: sfence.vma
0x80201044: csrw satp,a0 "写页表 MODE(0xa0) | ASID(0) | PPN(0x814e9), MMU被启用,这使得访问原有物理地址时触发异常并跳转到中断向量 0xffffffff80001048"
0x80201048: auipc a0,0x0
0x8020104c: addi a0,a0,124
0x80201050: csrw stvec,a0
0x80201054: auipc gp,0x12de
0x80201058: addi gp,gp,324
0x8020105c: csrw satp,a2
0x80201060: sfence.vma
0x80201064: ret

kernel setup_arch

Linux内核的codebase可以简单分成两个部分:架构相关部分和架构无关部分。为了支持多个架构,且最大限度地公用代码,又保留架构相关实现的灵活性,Linux内核的实现经过了精细的设计。内核底层对一些架构相关的操作进行了抽象,向内核通用代码提供了公共的接口。每一个内核支持的架构都对应一个arch/下的文件夹,里面存放着本架构相关的代码。

内核实现多架构支持的手段:

  • 条件编译。这个方法主要用在一些极端特殊场合,多用于驱动对特定平台的区别操作。内核提供了一些宏,用于检测当前架构。
  • weak函数。这个方法利用了ELF object文件中的weak symbol,具有这个属性的symbol在链接时,如果链接器可以在所有进行链接的object文件中找到同名symbol,则会用这个symbolweak symbol顶替掉。内核使用__weak(本质就是一个GCC扩展)标记weak函数。内核可以对所有架构实现一个通用的weak函数,如果有架构需要一个自己的版本,则可以直接定义,并将其顶替。
  • 平台相关函数。这类函数为强平台相关,内核一般定义一个共同函数原型及函数语意,由各架构自行实现该函数。这里其实也包括一部分宏。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
dtb_early_va = (void *)DTB_EARLY_BASE_VA + (dtb_pa & (PMD_SIZE - 1));
-+ setup_arch(char **cmdline_p)
\ -+ parse_dtb();
\ -+ early_init_dt_scan(dtb_early_va) "前面在创建临时页表时已经为 DTB_EARLY_BASE_VA - dtb_pa 建立了 4M (2个PMD_SIZE) 的大页映射"
\ - early_init_dt_verify(params); "检查设备树的合法性"
| -+ early_init_dt_scan_nodes()
\ - early_init_dt_scan_root(); "Initialize {size,address}-cells info"
| - early_init_dt_scan_chosen(boot_command_line);
"扫描设备树的`chosen`节点,获取内核命令行,initrd等关键信息,其中内核并命令行被保存在`boot_command_line`字符数组中"
| -+ early_init_dt_scan_memory();
"获取设备树中关于内存的描述,并调用`early_init_dt_add_memory_arch`"
\ -+ early_init_dt_add_memory_arch()
"使用了内核默认的实现,即将内核区域添加到`memblock`中。也就是说,RISC-V架构下的启动内存管理器是memblock,memblock的实现比较独立"
| - early_init_dt_check_for_usable_mem_range();
"Handle linux,usable-memory-range property"
| - parse_early_param(); "用于解析`early param`。内核中的`early param`都会特殊标记起来,保存在一个特殊的section里,
在内核启动初期从内核命令行解析出来。"
| -+ paging_init() "初始化页表"
\ -+ setup_bootmem(); "初始化memblock"
\ - phys_addr_t vmlinux_start = __pa_symbol(&_start);
"找到_start对应的物理地址, 这个地方因为已经启用了mmu, &_start相对寻址为虚拟地址, 需要将其转换为物理地址"
| - phys_addr_t vmlinux_end = __pa_symbol(&_end);
| - phys_ram_end = memblock_end_of_DRAM();
| - phys_ram_base = memblock_start_of_DRAM();
| - min_low_pfn = PFN_UP(phys_ram_base); "最小pfn 页号"
| - max_low_pfn = max_pfn = PFN_DOWN(phys_ram_end); "最大pfn页号"
| - high_memory = (void *)(__va(PFN_PHYS(max_low_pfn)));
| - dma32_phys_limit = min(4UL * SZ_1G, (unsigned long)PFN_PHYS(max_low_pfn));
| - memblock_reserve(dtb_early_pa, fdt_totalsize(dtb_early_va)); "保留early dtb 物理地址映射"
| - early_init_fdt_scan_reserved_mem();
"设备树中存在`reserved memory`的描述,通过该函数扫描fdt 中 reserved memory描述,然后将对应参数进行保留"
| - dma_contiguous_reserve(dma32_phys_limit);
| -+ setup_vm_final(); "将memblock中管理的内存添加到到`swapper_pg_dir`页表中,然后启用该页表"
\ - create_pgd_mapping(swapper_pg_dir, FIXADDR_START, __pa_symbol(fixmap_p4d), PGDIR_SIZE, PAGE_TABLE);
"swapper_pg_dir pgd 中建立 FIXADDR_START -> fixmap_p4d的页目录"
| -+ for_each_mem_range(i, &start, &end)
\ - map_size = best_map_size(start, end - start);
| -+ for (pa = start; pa < end; pa += map_size)
\ - create_pgd_mapping(swapper_pg_dir, va, pa, map_size, pgprot_from_va(va));
| - create_kernel_page_table(swapper_pg_dir, false); "第二次调用这个函数, 与第一次的区别early 为false, 这个地方的意思是又映射了一遍
只不过pgd 由early_pd_dir 变成了 swapper_pg_dir,
而early 为false, prot 由 PAGE_KERNEL_EXEC 变成pgprot_from_va(va),
由于此时的va还是查的early_pg_dir 的prot, 落到pmd 一级仍然是 PAGE_KERNEL_EXEC, 即这个地方还是大页"
| - csr_write(CSR_SATP, PFN_DOWN(__pa_symbol(swapper_pg_dir)) | satp_mode); "启用swapper_pg_dir 页表"
| - local_flush_tlb_all(); "清tlb"
| -+ pt_ops_set_late();
\ -+ pt_ops.alloc_pte = alloc_pte_late; "在 pt_ops_set_early 时不允许分配pte, 只能分配大页, 到这里允许分配pte了"
\ ---+ alloc_pte_late(va)
\ - vaddr = __get_free_page(GFP_KERNEL); "调用分配内存函数分配虚拟地址"
| - return __pa(vaddr); "返回pte的物理地址"
| - pt_ops.get_pmd_virt = get_pmd_virt_late;
| - pt_ops.alloc_pmd = alloc_pmd_late;
| - pt_ops.alloc_pud = alloc_pud_late;
| - pt_ops.alloc_p4d = alloc_p4d_late;
| - ...
| - unflatten_device_tree(); "该函数为OF模块的代码,目的是将设备树转换成更高效的内存中表示。"
| - sbi_init(); "sbi 探测版本 extension 支持feature, 设定sbi接口用来对接opensbi"
| -+ setup_smp(); "多核初始化, fdt cpu node"
\ ---+ cpu_ops[cpuid] = &cpu_ops_sbi; "注册cpu_ops"
\ ---+ kernel_init "主核初始化完后, 会起一个task kernel_init"
\ -+ kernel_init_freeable
\ -+ smp_init()
\ -+ bringup_nonboot_cpus(setup_max_cpus);
\ -+ for_each_present_cpu(cpu)
\ -+ cpu_up(cpu, CPUHP_ONLINE)
\ -+ cpuhp_up_callbacks
\ -+ cpuhp_invoke_callback_range
\ -+ cpuhp_invoke_callback
\ -+ bringup_cpu
\ -+ start_secondary_cpu(cpuid, task)
\ - boot_addr = __pa_symbol(secondary_start_sbi);
| - sbi_hsm_hart_start(hartid, boot_addr, hsm_data);
------------------------> other cpu <----------------------
\ - secondary_start_sbi
| - secondary_start_common
| -+ smp_callin
\ - notify_cpu_starting(curr_cpuid);
| - set_cpu_online(curr_cpuid, 1);
| - cpu_startup_entry(CPUHP_AP_ONLINE_IDLE); "idle 状态"
------------------------> other cpu <----------------------
| - riscv_fill_hwcap() "riscv hw 指令集探测 imafdc等"


前面看到内核在setup_vm中初始化了一个early_pg_dir页表,仅仅映射了内核所占用内存和一个FDT的fixmap,而paging_init中的setup_vm_final则是该操作的延续。首先明确使用两级初始化的原因:

  • 在没有读取设备树之前,内核是不知道物理内存的大小的。如果非要缩成一步,那么只能从内核所在内存结尾处开始,猜一个大小然后进行映射。这种实现有巨大的不确定性,并不是一个好的选择
  • 紧接上一条,这么做有可能需要映射一些不存在的内存区域,使得页表占用更多空间

所以setup_vm_final的操作本质上就是将memblock中管理的内存添加到到swapper_pg_dir页表中,然后启用该页表。

riscv smp 初始化boot

首先看下riscv kernel smp_call 从核的堆栈:

1
2
3
4
#0  sbi_hart_switch_mode (arg0=arg0@entry=0, arg1=4265325392, next_addr=2149584998, next_mode=1, next_virt=next_virt@entry=0) at /home/liguang/program/riscv-lab/opensbi/lib/sbi/sbi_hart.c:736
#1 0x000000008000087c in init_warmboot (hartid=<optimized out>, scratch=<optimized out>) at /home/liguang/program/riscv-lab/opensbi/lib/sbi/sbi_init.c:445
#2 sbi_init (scratch=0x80039000) at /home/liguang/program/riscv-lab/opensbi/lib/sbi/sbi_init.c:515
#3 0x00000000800003c4 in _start_warm () at /home/liguang/program/riscv-lab/opensbi/firmware/fw_base.S:501

从opensbi 开始说起:

其他cpu M-mode 停在了 opensbi的 sbi_hsm_hart_wait 处, 初始hsm_state 为 SBI_HSM_STATE_STOPPED

1
2
3
4
struct sbi_hsm_data *hdata = sbi_scratch_offset_ptr(scratch, hart_data_offset);
while (atomic_read(&hdata->state) != SBI_HSM_STATE_START_PENDING) {
wfi();
};

当主cpu 从kernel sbi_hsm_hart_start 发送 SBI_EXT_HSM_HART_START extension 后, 将hartid saddr=secondary_start_sbi 以及封装的priv 私有数据地址 发送给了opensbi.

主核的 opensbi 接收后将saddr 保存到了 opensbi 为hart 预留的scratch 空间的next_addr处, 将hsm_state变为 SBI_HSM_STATE_START_PENDING状态, 接着调用 ipi 给hartid 发送ipi中断.

1
2
3
4
5
6
rscratch = sbi_hartid_to_scratch(hartid);
rscratch->next_arg1 = priv;
rscratch->next_addr = saddr;
rscratch->next_mode = smode;
hdata = sbi_scratch_offset_ptr(rscratch, hart_data_offset);
hstate = atomic_cmpxchg(&hdata->state, SBI_HSM_STATE_STOPPED, SBI_HSM_STATE_START_PENDING);

hartid 的从核收到ipi 后, 设置了 MIP.SSIE位

1
2
3
4
static void sbi_ipi_process_smode(struct sbi_scratch *scratch)
{
csr_set(CSR_MIP, MIP_SSIP);
}

退出中断上下文, 返回到了 sbi_hsm_hart_wait继续处理, 此时hsm_state为 SBI_HSM_STATE_START_PENDING, 退出wait 循环
接着执行sbi_hsm_hart_wait 后面的函数, 最终执行到 sbi_hart_switch_mode

1
2
3
4
5
6
7
8
9
10
11
sbi_hart_switch_mode(hartid, scratch->next_arg1, scratch->next_addr,
scratch->next_mode, FALSE);
{
csr_write(CSR_STVEC, next_addr);
csr_write(CSR_SSCRATCH, 0);
csr_write(CSR_SIE, 0);
csr_write(CSR_SATP, 0);
register unsigned long a0 asm("a0") = arg0; "hartid"
register unsigned long a1 asm("a1") = arg1; "scratch->next_arg1 = priv, priv为kernel传来的私有数据地址"
__asm__ __volatile__("mret" : : "r"(a0), "r"(a1)); //跳到next_addr 即 secondary_start_sbi 执行
}